Skip to main content

ff_decode/audio/
builder.rs

1//! Audio decoder builder for constructing audio decoders with custom configuration.
2//!
3//! This module provides the [`AudioDecoderBuilder`] type which enables fluent
4//! configuration of audio decoders. Use [`AudioDecoder::open()`] to start building.
5
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use ff_format::{AudioFrame, AudioStreamInfo, ContainerInfo, NetworkOptions, SampleFormat};
10
11use crate::audio::decoder_inner::AudioDecoderInner;
12use crate::error::DecodeError;
13
14/// Builder for configuring and constructing an [`AudioDecoder`].
15///
16/// This struct provides a fluent interface for setting up decoder options
17/// before opening an audio file. It is created by calling [`AudioDecoder::open()`].
18///
19/// # Examples
20///
21/// ## Basic Usage
22///
23/// ```ignore
24/// use ff_decode::AudioDecoder;
25///
26/// let decoder = AudioDecoder::open("audio.mp3")?
27///     .build()?;
28/// ```
29///
30/// ## With Custom Format and Sample Rate
31///
32/// ```ignore
33/// use ff_decode::AudioDecoder;
34/// use ff_format::SampleFormat;
35///
36/// let decoder = AudioDecoder::open("audio.mp3")?
37///     .output_format(SampleFormat::F32)
38///     .output_sample_rate(48000)
39///     .build()?;
40/// ```
41#[derive(Debug)]
42pub struct AudioDecoderBuilder {
43    /// Path to the media file
44    path: PathBuf,
45    /// Output sample format (None = use source format)
46    output_format: Option<SampleFormat>,
47    /// Output sample rate (None = use source sample rate)
48    output_sample_rate: Option<u32>,
49    /// Output channel count (None = use source channel count)
50    output_channels: Option<u32>,
51    /// Network options for URL-based sources (None = use defaults)
52    network_opts: Option<NetworkOptions>,
53}
54
55impl AudioDecoderBuilder {
56    /// Creates a new builder for the specified file path.
57    ///
58    /// This is an internal constructor; use [`AudioDecoder::open()`] instead.
59    pub(crate) fn new(path: PathBuf) -> Self {
60        Self {
61            path,
62            output_format: None,
63            output_sample_rate: None,
64            output_channels: None,
65            network_opts: None,
66        }
67    }
68
69    /// Sets the output sample format for decoded frames.
70    ///
71    /// If not set, frames are returned in the source format. Setting an
72    /// output format enables automatic conversion during decoding.
73    ///
74    /// # Common Formats
75    ///
76    /// - [`SampleFormat::F32`] - 32-bit float, most common for editing
77    /// - [`SampleFormat::I16`] - 16-bit integer, CD quality
78    /// - [`SampleFormat::F32p`] - Planar 32-bit float, efficient for processing
79    ///
80    /// # Examples
81    ///
82    /// ```ignore
83    /// use ff_decode::AudioDecoder;
84    /// use ff_format::SampleFormat;
85    ///
86    /// let decoder = AudioDecoder::open("audio.mp3")?
87    ///     .output_format(SampleFormat::F32)
88    ///     .build()?;
89    /// ```
90    #[must_use]
91    pub fn output_format(mut self, format: SampleFormat) -> Self {
92        self.output_format = Some(format);
93        self
94    }
95
96    /// Sets the output sample rate in Hz.
97    ///
98    /// If not set, frames are returned at the source sample rate. Setting an
99    /// output sample rate enables automatic resampling during decoding.
100    ///
101    /// # Common Sample Rates
102    ///
103    /// - 44100 Hz - CD quality audio
104    /// - 48000 Hz - Professional audio, most common in video
105    /// - 96000 Hz - High-resolution audio
106    ///
107    /// # Examples
108    ///
109    /// ```ignore
110    /// use ff_decode::AudioDecoder;
111    ///
112    /// // Resample to 48kHz
113    /// let decoder = AudioDecoder::open("audio.mp3")?
114    ///     .output_sample_rate(48000)
115    ///     .build()?;
116    /// ```
117    #[must_use]
118    pub fn output_sample_rate(mut self, sample_rate: u32) -> Self {
119        self.output_sample_rate = Some(sample_rate);
120        self
121    }
122
123    /// Sets the output channel count.
124    ///
125    /// If not set, frames are returned with the source channel count. Setting an
126    /// output channel count enables automatic channel remixing during decoding.
127    ///
128    /// # Common Channel Counts
129    ///
130    /// - 1 - Mono
131    /// - 2 - Stereo
132    /// - 6 - 5.1 surround sound
133    ///
134    /// # Examples
135    ///
136    /// ```ignore
137    /// use ff_decode::AudioDecoder;
138    ///
139    /// // Convert to stereo
140    /// let decoder = AudioDecoder::open("audio.mp3")?
141    ///     .output_channels(2)
142    ///     .build()?;
143    /// ```
144    #[must_use]
145    pub fn output_channels(mut self, channels: u32) -> Self {
146        self.output_channels = Some(channels);
147        self
148    }
149
150    /// Sets network options for URL-based audio sources (HTTP, RTSP, RTMP, etc.).
151    ///
152    /// When set, the builder skips the file-existence check and passes connect
153    /// and read timeouts to `avformat_open_input` via an `AVDictionary`.
154    /// Call this before `.build()` when opening `rtmp://`, `rtsp://`, `http://`,
155    /// `https://`, `udp://`, `srt://`, or `rtp://` URLs.
156    ///
157    /// # HLS / M3U8 Playlists
158    ///
159    /// Audio-only HLS playlists (`.m3u8` pointing to AAC or MP3 segments) are
160    /// detected automatically by `FFmpeg`. Pass the full HTTP(S) URL:
161    ///
162    /// ```ignore
163    /// use ff_decode::AudioDecoder;
164    /// use ff_format::NetworkOptions;
165    ///
166    /// let decoder = AudioDecoder::open("https://example.com/audio/index.m3u8")
167    ///     .network(NetworkOptions::default())
168    ///     .build()?;
169    /// ```
170    ///
171    /// # UDP / MPEG-TS
172    ///
173    /// `udp://` URLs are always live — `is_live()` returns `true` and seeking
174    /// is not supported. Two extra `AVDictionary` options are set automatically
175    /// to reduce packet loss on high-bitrate streams:
176    ///
177    /// | Option | Value | Reason |
178    /// |---|---|---|
179    /// | `buffer_size` | `65536` | Enlarges the UDP receive buffer |
180    /// | `overrun_nonfatal` | `1` | Discards excess data instead of erroring |
181    ///
182    /// # SRT (Secure Reliable Transport)
183    ///
184    /// SRT URLs (`srt://host:port`) require the `srt` feature flag **and** a
185    /// libsrt-enabled `FFmpeg` build.  Enable the feature in `Cargo.toml`:
186    ///
187    /// ```toml
188    /// [dependencies]
189    /// ff-decode = { version = "*", features = ["srt"] }
190    /// ```
191    ///
192    /// Without the `srt` feature, opening an `srt://` URL returns
193    /// [`DecodeError::ConnectionFailed`]. If the feature is enabled but the
194    /// linked `FFmpeg` was not built with `--enable-libsrt`, the same error is
195    /// returned with a message directing you to rebuild `FFmpeg`.
196    ///
197    /// ```ignore
198    /// use ff_decode::AudioDecoder;
199    /// use ff_format::NetworkOptions;
200    ///
201    /// let decoder = AudioDecoder::open("srt://ingest.example.com:4200")
202    ///     .network(NetworkOptions::default())
203    ///     .build()?;
204    /// ```
205    ///
206    /// # Credentials
207    ///
208    /// HTTP basic-auth credentials must be embedded directly in the URL:
209    /// `https://user:password@cdn.example.com/audio/index.m3u8`.
210    /// The password is redacted in log output.
211    ///
212    /// # DRM Limitation
213    ///
214    /// DRM-protected streams are **not** supported:
215    /// - HLS: `FairPlay`, Widevine, AES-128 with external key servers
216    /// - DASH: CENC, `PlayReady`, Widevine
217    ///
218    /// `FFmpeg` can parse the manifest and fetch segments, but key delivery
219    /// to a DRM license server is outside the scope of this API.
220    ///
221    /// # Examples
222    ///
223    /// ```ignore
224    /// use ff_decode::AudioDecoder;
225    /// use ff_format::NetworkOptions;
226    /// use std::time::Duration;
227    ///
228    /// let decoder = AudioDecoder::open("http://stream.example.com/audio.aac")
229    ///     .network(NetworkOptions {
230    ///         connect_timeout: Duration::from_secs(5),
231    ///         ..Default::default()
232    ///     })
233    ///     .build()?;
234    /// ```
235    #[must_use]
236    pub fn network(mut self, opts: NetworkOptions) -> Self {
237        self.network_opts = Some(opts);
238        self
239    }
240
241    /// Returns the configured file path.
242    #[must_use]
243    pub fn path(&self) -> &Path {
244        &self.path
245    }
246
247    /// Returns the configured output format, if any.
248    #[must_use]
249    pub fn get_output_format(&self) -> Option<SampleFormat> {
250        self.output_format
251    }
252
253    /// Returns the configured output sample rate, if any.
254    #[must_use]
255    pub fn get_output_sample_rate(&self) -> Option<u32> {
256        self.output_sample_rate
257    }
258
259    /// Returns the configured output channel count, if any.
260    #[must_use]
261    pub fn get_output_channels(&self) -> Option<u32> {
262        self.output_channels
263    }
264
265    /// Builds the audio decoder with the configured options.
266    ///
267    /// This method opens the media file, initializes the decoder context,
268    /// and prepares for frame decoding.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if:
273    /// - The file cannot be found ([`DecodeError::FileNotFound`])
274    /// - The file contains no audio stream ([`DecodeError::NoAudioStream`])
275    /// - The codec is not supported ([`DecodeError::UnsupportedCodec`])
276    /// - Other `FFmpeg` errors occur ([`DecodeError::Ffmpeg`])
277    ///
278    /// # Examples
279    ///
280    /// ```ignore
281    /// use ff_decode::AudioDecoder;
282    ///
283    /// let decoder = AudioDecoder::open("audio.mp3")?
284    ///     .build()?;
285    ///
286    /// // Start decoding
287    /// for result in &mut decoder {
288    ///     let frame = result?;
289    ///     // Process frame...
290    /// }
291    /// ```
292    pub fn build(self) -> Result<AudioDecoder, DecodeError> {
293        // Network URLs skip the file-existence check (literal path does not exist).
294        let is_network_url = self.path.to_str().is_some_and(crate::network::is_url);
295        if !is_network_url && !self.path.exists() {
296            return Err(DecodeError::FileNotFound {
297                path: self.path.clone(),
298            });
299        }
300
301        // Create the decoder inner
302        let (inner, stream_info, container_info) = AudioDecoderInner::new(
303            &self.path,
304            self.output_format,
305            self.output_sample_rate,
306            self.output_channels,
307            self.network_opts,
308        )?;
309
310        Ok(AudioDecoder {
311            path: self.path,
312            inner,
313            stream_info,
314            container_info,
315            fused: false,
316        })
317    }
318}
319
320/// An audio decoder for extracting audio frames from media files.
321///
322/// The decoder provides frame-by-frame access to audio content with support
323/// for resampling and format conversion.
324///
325/// # Construction
326///
327/// Use [`AudioDecoder::open()`] to create a builder, then call [`AudioDecoderBuilder::build()`]:
328///
329/// ```ignore
330/// use ff_decode::AudioDecoder;
331/// use ff_format::SampleFormat;
332///
333/// let decoder = AudioDecoder::open("audio.mp3")?
334///     .output_format(SampleFormat::F32)
335///     .output_sample_rate(48000)
336///     .build()?;
337/// ```
338///
339/// # Frame Decoding
340///
341/// Frames can be decoded one at a time or using an iterator:
342///
343/// ```ignore
344/// // Decode one frame
345/// if let Some(frame) = decoder.decode_one()? {
346///     println!("Frame with {} samples", frame.samples());
347/// }
348///
349/// // Iterator form — AudioDecoder implements Iterator directly
350/// for result in &mut decoder {
351///     let frame = result?;
352///     // Process frame...
353/// }
354/// ```
355///
356/// # Seeking
357///
358/// The decoder supports seeking to specific positions:
359///
360/// ```ignore
361/// use std::time::Duration;
362///
363/// // Seek to 30 seconds
364/// decoder.seek(Duration::from_secs(30))?;
365/// ```
366pub struct AudioDecoder {
367    /// Path to the media file
368    path: PathBuf,
369    /// Internal decoder state
370    inner: AudioDecoderInner,
371    /// Audio stream information
372    stream_info: AudioStreamInfo,
373    /// Container-level metadata
374    container_info: ContainerInfo,
375    /// Set to `true` after a decoding error; causes [`Iterator::next`] to return `None`.
376    fused: bool,
377}
378
379impl AudioDecoder {
380    /// Opens a media file and returns a builder for configuring the decoder.
381    ///
382    /// This is the entry point for creating a decoder. The returned builder
383    /// allows setting options before the decoder is fully initialized.
384    ///
385    /// # Arguments
386    ///
387    /// * `path` - Path to the media file to decode.
388    ///
389    /// # Examples
390    ///
391    /// ```ignore
392    /// use ff_decode::AudioDecoder;
393    ///
394    /// // Simple usage
395    /// let decoder = AudioDecoder::open("audio.mp3")?
396    ///     .build()?;
397    ///
398    /// // With options
399    /// let decoder = AudioDecoder::open("audio.mp3")?
400    ///     .output_format(SampleFormat::F32)
401    ///     .output_sample_rate(48000)
402    ///     .build()?;
403    /// ```
404    ///
405    /// # Note
406    ///
407    /// This method does not validate that the file exists or is a valid
408    /// media file. Validation occurs when [`AudioDecoderBuilder::build()`] is called.
409    pub fn open(path: impl AsRef<Path>) -> AudioDecoderBuilder {
410        AudioDecoderBuilder::new(path.as_ref().to_path_buf())
411    }
412
413    // =========================================================================
414    // Information Methods
415    // =========================================================================
416
417    /// Returns the audio stream information.
418    ///
419    /// This contains metadata about the audio stream including sample rate,
420    /// channel count, codec, and format characteristics.
421    #[must_use]
422    pub fn stream_info(&self) -> &AudioStreamInfo {
423        &self.stream_info
424    }
425
426    /// Returns the sample rate in Hz.
427    #[must_use]
428    pub fn sample_rate(&self) -> u32 {
429        self.stream_info.sample_rate()
430    }
431
432    /// Returns the number of audio channels.
433    ///
434    /// The type is `u32` to match `FFmpeg` and professional audio APIs. When
435    /// integrating with `rodio` or `cpal` (which require `u16`), cast with
436    /// `decoder.channels() as u16` — channel counts never exceed `u16::MAX`
437    /// in practice.
438    #[must_use]
439    pub fn channels(&self) -> u32 {
440        self.stream_info.channels()
441    }
442
443    /// Returns the total duration of the audio.
444    ///
445    /// Returns [`Duration::ZERO`] if duration is unknown.
446    #[must_use]
447    pub fn duration(&self) -> Duration {
448        self.stream_info.duration().unwrap_or(Duration::ZERO)
449    }
450
451    /// Returns the total duration of the audio, or `None` for live streams
452    /// or formats that do not carry duration information.
453    #[must_use]
454    pub fn duration_opt(&self) -> Option<Duration> {
455        self.stream_info.duration()
456    }
457
458    /// Returns container-level metadata (format name, bitrate, stream count).
459    #[must_use]
460    pub fn container_info(&self) -> &ContainerInfo {
461        &self.container_info
462    }
463
464    /// Returns the current playback position.
465    #[must_use]
466    pub fn position(&self) -> Duration {
467        self.inner.position()
468    }
469
470    /// Returns `true` if the end of stream has been reached.
471    #[must_use]
472    pub fn is_eof(&self) -> bool {
473        self.inner.is_eof()
474    }
475
476    /// Returns the file path being decoded.
477    #[must_use]
478    pub fn path(&self) -> &Path {
479        &self.path
480    }
481
482    // =========================================================================
483    // Decoding Methods
484    // =========================================================================
485
486    /// Decodes the next audio frame.
487    ///
488    /// This method reads and decodes a single frame from the audio stream.
489    ///
490    /// # Returns
491    ///
492    /// - `Ok(Some(frame))` - A frame was successfully decoded
493    /// - `Ok(None)` - End of stream reached, no more frames
494    /// - `Err(_)` - An error occurred during decoding
495    ///
496    /// # Errors
497    ///
498    /// Returns [`DecodeError`] if:
499    /// - Reading from the file fails
500    /// - Decoding the frame fails
501    /// - Sample format conversion fails
502    ///
503    /// # Examples
504    ///
505    /// ```ignore
506    /// use ff_decode::AudioDecoder;
507    ///
508    /// let mut decoder = AudioDecoder::open("audio.mp3")?.build()?;
509    ///
510    /// while let Some(frame) = decoder.decode_one()? {
511    ///     println!("Frame with {} samples", frame.samples());
512    ///     // Process frame...
513    /// }
514    /// ```
515    pub fn decode_one(&mut self) -> Result<Option<AudioFrame>, DecodeError> {
516        self.inner.decode_one()
517    }
518
519    /// Decodes all frames and returns their raw PCM data.
520    ///
521    /// This method decodes the entire audio file and returns all samples
522    /// as a contiguous byte buffer.
523    ///
524    /// # Performance
525    ///
526    /// - Memory scales with audio duration and quality
527    /// - For 10 minutes of stereo 48kHz F32 audio: ~110 MB
528    ///
529    /// # Returns
530    ///
531    /// A byte vector containing all audio samples in the configured output format.
532    ///
533    /// # Errors
534    ///
535    /// Returns [`DecodeError`] if:
536    /// - Decoding any frame fails
537    /// - The file cannot be read
538    ///
539    /// # Examples
540    ///
541    /// ```ignore
542    /// use ff_decode::AudioDecoder;
543    /// use ff_format::SampleFormat;
544    ///
545    /// let mut decoder = AudioDecoder::open("audio.mp3")?
546    ///     .output_format(SampleFormat::F32)
547    ///     .build()?;
548    ///
549    /// let samples = decoder.decode_all()?;
550    /// println!("Decoded {} bytes", samples.len());
551    /// ```
552    ///
553    /// # Memory Usage
554    ///
555    /// Stereo 48kHz F32 audio:
556    /// - 1 minute: ~11 MB
557    /// - 5 minutes: ~55 MB
558    /// - 10 minutes: ~110 MB
559    pub fn decode_all(&mut self) -> Result<Vec<u8>, DecodeError> {
560        let mut buffer = Vec::new();
561
562        while let Some(frame) = self.decode_one()? {
563            // Collect samples from all planes
564            for plane in frame.planes() {
565                buffer.extend_from_slice(plane);
566            }
567        }
568
569        Ok(buffer)
570    }
571
572    /// Decodes all frames within a specified time range.
573    ///
574    /// This method seeks to the start position and decodes all frames until
575    /// the end position is reached. Frames outside the range are skipped.
576    ///
577    /// # Arguments
578    ///
579    /// * `start` - Start of the time range (inclusive).
580    /// * `end` - End of the time range (exclusive).
581    ///
582    /// # Returns
583    ///
584    /// A byte vector containing audio samples within `[start, end)`.
585    ///
586    /// # Errors
587    ///
588    /// Returns [`DecodeError`] if:
589    /// - Seeking to the start position fails
590    /// - Decoding frames fails
591    /// - The time range is invalid (start >= end)
592    ///
593    /// # Examples
594    ///
595    /// ```ignore
596    /// use ff_decode::AudioDecoder;
597    /// use std::time::Duration;
598    ///
599    /// let mut decoder = AudioDecoder::open("audio.mp3")?.build()?;
600    ///
601    /// // Decode audio from 5s to 10s
602    /// let samples = decoder.decode_range(
603    ///     Duration::from_secs(5),
604    ///     Duration::from_secs(10),
605    /// )?;
606    ///
607    /// println!("Decoded {} bytes", samples.len());
608    /// ```
609    pub fn decode_range(&mut self, start: Duration, end: Duration) -> Result<Vec<u8>, DecodeError> {
610        // Validate range
611        if start >= end {
612            return Err(DecodeError::DecodingFailed {
613                timestamp: Some(start),
614                reason: format!(
615                    "Invalid time range: start ({start:?}) must be before end ({end:?})"
616                ),
617            });
618        }
619
620        // Seek to start position (keyframe mode for efficiency)
621        self.seek(start, crate::SeekMode::Keyframe)?;
622
623        // Collect frames in the range
624        let mut buffer = Vec::new();
625
626        while let Some(frame) = self.decode_one()? {
627            let frame_time = frame.timestamp().as_duration();
628
629            // Stop if we've passed the end of the range
630            if frame_time >= end {
631                break;
632            }
633
634            // Only collect frames within the range
635            if frame_time >= start {
636                for plane in frame.planes() {
637                    buffer.extend_from_slice(plane);
638                }
639            }
640        }
641
642        Ok(buffer)
643    }
644
645    // =========================================================================
646    // Seeking Methods
647    // =========================================================================
648
649    /// Seeks to a specified position in the audio stream.
650    ///
651    /// This method performs efficient seeking without reopening the file.
652    ///
653    /// # Arguments
654    ///
655    /// * `position` - Target position to seek to.
656    /// * `mode` - Seek mode (Keyframe, Exact, or Backward).
657    ///
658    /// # Errors
659    ///
660    /// Returns [`DecodeError::SeekFailed`] if:
661    /// - The target position is beyond the audio duration
662    /// - The file format doesn't support seeking
663    /// - The seek operation fails internally
664    ///
665    /// # Examples
666    ///
667    /// ```ignore
668    /// use ff_decode::{AudioDecoder, SeekMode};
669    /// use std::time::Duration;
670    ///
671    /// let mut decoder = AudioDecoder::open("audio.mp3")?.build()?;
672    ///
673    /// // Seek to 30 seconds with keyframe mode (fast)
674    /// decoder.seek(Duration::from_secs(30), SeekMode::Keyframe)?;
675    ///
676    /// // Seek to exact position (slower but precise)
677    /// decoder.seek(Duration::from_secs(45), SeekMode::Exact)?;
678    ///
679    /// // Decode next frame
680    /// if let Some(frame) = decoder.decode_one()? {
681    ///     println!("Frame at {:?}", frame.timestamp().as_duration());
682    /// }
683    /// ```
684    pub fn seek(&mut self, position: Duration, mode: crate::SeekMode) -> Result<(), DecodeError> {
685        if self.inner.is_live() {
686            return Err(DecodeError::SeekNotSupported);
687        }
688        self.inner.seek(position, mode)
689    }
690
691    /// Returns `true` if the source is a live or streaming input.
692    ///
693    /// Live sources (HLS live playlists, RTMP, RTSP, MPEG-TS) have the
694    /// `AVFMT_TS_DISCONT` flag set on their `AVInputFormat`. Seeking is not
695    /// supported on live sources — [`AudioDecoder::seek`] will return
696    /// [`DecodeError::SeekNotSupported`].
697    #[must_use]
698    pub fn is_live(&self) -> bool {
699        self.inner.is_live()
700    }
701
702    /// Flushes the decoder's internal buffers.
703    ///
704    /// This method clears any cached frames and resets the decoder state.
705    /// The decoder is ready to receive new packets after flushing.
706    pub fn flush(&mut self) {
707        self.inner.flush();
708    }
709}
710
711impl Iterator for AudioDecoder {
712    type Item = Result<AudioFrame, DecodeError>;
713
714    fn next(&mut self) -> Option<Self::Item> {
715        if self.fused {
716            return None;
717        }
718        match self.decode_one() {
719            Ok(Some(frame)) => Some(Ok(frame)),
720            Ok(None) => None,
721            Err(e) => {
722                self.fused = true;
723                Some(Err(e))
724            }
725        }
726    }
727}
728
729impl std::iter::FusedIterator for AudioDecoder {}
730
731#[cfg(test)]
732mod tests {
733    use super::*;
734    use std::path::PathBuf;
735
736    #[test]
737    fn test_builder_default_values() {
738        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3"));
739
740        assert_eq!(builder.path(), Path::new("test.mp3"));
741        assert!(builder.get_output_format().is_none());
742        assert!(builder.get_output_sample_rate().is_none());
743        assert!(builder.get_output_channels().is_none());
744    }
745
746    #[test]
747    fn test_builder_output_format() {
748        let builder =
749            AudioDecoderBuilder::new(PathBuf::from("test.mp3")).output_format(SampleFormat::F32);
750
751        assert_eq!(builder.get_output_format(), Some(SampleFormat::F32));
752    }
753
754    #[test]
755    fn test_builder_output_sample_rate() {
756        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3")).output_sample_rate(48000);
757
758        assert_eq!(builder.get_output_sample_rate(), Some(48000));
759    }
760
761    #[test]
762    fn test_builder_output_channels() {
763        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3")).output_channels(2);
764
765        assert_eq!(builder.get_output_channels(), Some(2));
766    }
767
768    #[test]
769    fn test_builder_chaining() {
770        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3"))
771            .output_format(SampleFormat::F32)
772            .output_sample_rate(48000)
773            .output_channels(2);
774
775        assert_eq!(builder.get_output_format(), Some(SampleFormat::F32));
776        assert_eq!(builder.get_output_sample_rate(), Some(48000));
777        assert_eq!(builder.get_output_channels(), Some(2));
778    }
779
780    #[test]
781    fn test_decoder_open() {
782        let builder = AudioDecoder::open("audio.mp3");
783        assert_eq!(builder.path(), Path::new("audio.mp3"));
784    }
785
786    #[test]
787    fn test_build_file_not_found() {
788        let result = AudioDecoder::open("nonexistent_file_12345.mp3").build();
789
790        assert!(result.is_err());
791        match result {
792            Err(DecodeError::FileNotFound { path }) => {
793                assert!(
794                    path.to_string_lossy()
795                        .contains("nonexistent_file_12345.mp3")
796                );
797            }
798            Err(e) => panic!("Expected FileNotFound error, got: {e:?}"),
799            Ok(_) => panic!("Expected error, got Ok"),
800        }
801    }
802
803    #[test]
804    fn network_setter_should_store_options() {
805        let opts = NetworkOptions::default();
806        let builder = AudioDecoderBuilder::new(PathBuf::from("test.mp3")).network(opts.clone());
807        assert_eq!(builder.network_opts, Some(opts));
808    }
809
810    #[test]
811    fn build_should_bypass_file_existence_check_for_network_url() {
812        // A network URL that clearly does not exist locally should not return
813        // FileNotFound — it will return a different error (or succeed) from
814        // FFmpeg's network layer. The important thing is that FileNotFound is
815        // NOT returned.
816        let result = AudioDecoder::open("http://192.0.2.1/nonexistent.aac").build();
817        assert!(
818            !matches!(result, Err(DecodeError::FileNotFound { .. })),
819            "FileNotFound must not be returned for network URLs"
820        );
821    }
822}