oxidio_core/
decoder.rs

1//! Audio decoding via Symphonia
2//!
3//! Handles decoding of various audio formats into raw PCM samples.
4
5use std::fs::File;
6use std::path::Path;
7
8use symphonia::core::audio::{ SampleBuffer, SignalSpec };
9use symphonia::core::codecs::{ Decoder as SymphoniaDecoder, DecoderOptions, CODEC_TYPE_NULL };
10use symphonia::core::formats::{ FormatOptions, FormatReader, SeekMode, SeekTo };
11use symphonia::core::io::{ MediaSourceStream, MediaSourceStreamOptions };
12use symphonia::core::meta::{ MetadataOptions, StandardTagKey };
13use symphonia::core::probe::ProbedMetadata;
14use symphonia::core::probe::Hint;
15use symphonia::core::units::Time;
16use thiserror::Error;
17
18
19/// Audio metadata extracted from the file.
20#[derive( Debug, Clone, Default )]
21pub struct AudioMetadata {
22    pub title: Option<String>,
23    pub artist: Option<String>,
24    pub album: Option<String>,
25    pub album_artist: Option<String>,
26    pub track_number: Option<u32>,
27    pub genre: Option<String>,
28    pub year: Option<u32>,
29    pub codec: Option<String>,
30    pub bitrate: Option<u32>,
31    pub sample_rate: Option<u32>,
32    pub channels: Option<u32>,
33}
34
35
36/// Errors that can occur during decoding.
37#[derive( Debug, Error )]
38pub enum DecoderError {
39    #[error( "Failed to open file: {0}" )]
40    FileOpen( #[from] std::io::Error ),
41
42    #[error( "Unsupported format" )]
43    UnsupportedFormat,
44
45    #[error( "No audio tracks found" )]
46    NoAudioTrack,
47
48    #[error( "Decoder creation failed: {0}" )]
49    DecoderCreation( String ),
50
51    #[error( "Decode error: {0}" )]
52    Decode( String ),
53
54    #[error( "Seek error: {0}" )]
55    Seek( String ),
56}
57
58
59/// Audio decoder wrapper around Symphonia.
60pub struct Decoder {
61    format_reader: Box<dyn FormatReader>,
62    decoder: Box<dyn SymphoniaDecoder>,
63    track_id: u32,
64    sample_rate: u32,
65    channels: usize,
66    sample_buf: Option<SampleBuffer<f32>>,
67    duration: Option<f64>,
68    /// Metadata from probe result (ID3 tags, etc.)
69    probe_metadata: ProbedMetadata,
70}
71
72
73impl Decoder {
74    /// Opens an audio file for decoding.
75    ///
76    /// Supports SMB/UNC paths transparently via std::fs.
77    pub fn open( path: &Path ) -> Result<Self, DecoderError> {
78        // Use larger buffer for network paths (SMB)
79        let buffer_len = if path.starts_with( r"\\" ) {
80            256 * 1024 // 256KB for network paths
81        } else {
82            64 * 1024 // 64KB for local paths
83        };
84
85        let file = File::open( path )?;
86        let mss_opts = MediaSourceStreamOptions { buffer_len };
87        let mss = MediaSourceStream::new( Box::new( file ), mss_opts );
88
89        // Provide hint based on file extension
90        let mut hint = Hint::new();
91        if let Some( ext ) = path.extension().and_then( |e| e.to_str() ) {
92            hint.with_extension( ext );
93        }
94
95        let format_opts = FormatOptions::default();
96        let metadata_opts = MetadataOptions::default();
97
98        // Probe the file to determine format
99        let probed = symphonia::default::get_probe()
100            .format( &hint, mss, &format_opts, &metadata_opts )
101            .map_err( |_| DecoderError::UnsupportedFormat )?;
102
103        // Capture metadata from the probe result (ID3 tags, etc.)
104        let probe_metadata = probed.metadata;
105        let format_reader = probed.format;
106
107        // Find the first audio track
108        let track = format_reader
109            .tracks()
110            .iter()
111            .find( |t| t.codec_params.codec != CODEC_TYPE_NULL )
112            .ok_or( DecoderError::NoAudioTrack )?;
113
114        let track_id = track.id;
115        let codec_params = &track.codec_params;
116
117        let sample_rate = codec_params.sample_rate.unwrap_or( 44100 );
118        let channels = codec_params.channels.map( |c| c.count() ).unwrap_or( 2 );
119
120        // Calculate duration if available
121        let duration = codec_params.n_frames.map( |frames| {
122            frames as f64 / sample_rate as f64
123        });
124
125        tracing::info!(
126            "Opened audio: {} Hz, {} channels, duration: {:?}s",
127            sample_rate,
128            channels,
129            duration
130        );
131
132        // Create the decoder
133        let decoder_opts = DecoderOptions::default();
134        let decoder = symphonia::default::get_codecs()
135            .make( codec_params, &decoder_opts )
136            .map_err( |e| DecoderError::DecoderCreation( e.to_string() ) )?;
137
138        Ok( Self {
139            format_reader,
140            decoder,
141            track_id,
142            sample_rate,
143            channels,
144            sample_buf: None,
145            duration,
146            probe_metadata,
147        })
148    }
149
150
151    /// Returns the sample rate of the audio.
152    pub fn sample_rate( &self ) -> u32 {
153        self.sample_rate
154    }
155
156
157    /// Returns the number of channels.
158    pub fn channels( &self ) -> usize {
159        self.channels
160    }
161
162
163    /// Returns the duration in seconds, if known.
164    pub fn duration( &self ) -> Option<f64> {
165        self.duration
166    }
167
168
169    /// Extracts metadata from the audio file.
170    pub fn metadata( &mut self ) -> AudioMetadata {
171        let mut meta = AudioMetadata::default();
172
173        // Helper to extract tags from a metadata revision
174        let extract_tags = |meta: &mut AudioMetadata, tags: &[symphonia::core::meta::Tag]| {
175            for tag in tags {
176                if let Some( std_key ) = tag.std_key {
177                    let value = tag.value.to_string();
178                    match std_key {
179                        StandardTagKey::TrackTitle => {
180                            if meta.title.is_none() {
181                                meta.title = Some( value );
182                            }
183                        }
184                        StandardTagKey::Artist => {
185                            if meta.artist.is_none() {
186                                meta.artist = Some( value );
187                            }
188                        }
189                        StandardTagKey::Album => {
190                            if meta.album.is_none() {
191                                meta.album = Some( value );
192                            }
193                        }
194                        StandardTagKey::AlbumArtist => {
195                            if meta.album_artist.is_none() {
196                                meta.album_artist = Some( value );
197                            }
198                        }
199                        StandardTagKey::TrackNumber => {
200                            if meta.track_number.is_none() {
201                                meta.track_number = value.parse().ok();
202                            }
203                        }
204                        StandardTagKey::Genre => {
205                            if meta.genre.is_none() {
206                                meta.genre = Some( value );
207                            }
208                        }
209                        StandardTagKey::Date | StandardTagKey::ReleaseDate => {
210                            if meta.year.is_none() {
211                                // Extract year from date string (e.g., "2023" or "2023-01-15")
212                                if let Some( year_str ) = value.split( '-' ).next() {
213                                    meta.year = year_str.parse().ok();
214                                }
215                            }
216                        }
217                        _ => {}
218                    }
219                }
220            }
221        };
222
223        // First check probe metadata (ID3 tags, etc.)
224        if let Some( metadata_log ) = self.probe_metadata.get() {
225            if let Some( metadata_rev ) = metadata_log.current() {
226                extract_tags( &mut meta, metadata_rev.tags() );
227            }
228        }
229
230        // Then check format reader metadata (may have additional tags)
231        if let Some( metadata_rev ) = self.format_reader.metadata().current() {
232            extract_tags( &mut meta, metadata_rev.tags() );
233        }
234
235        // Add audio format information
236        meta.sample_rate = Some( self.sample_rate );
237        meta.channels = Some( self.channels as u32 );
238
239        // Get codec and bitrate from track info
240        if let Some( track ) = self.format_reader.tracks().iter().find( |t| t.id == self.track_id ) {
241            // Get codec name
242            let codec_type = track.codec_params.codec;
243            meta.codec = Some( format!( "{:?}", codec_type ).replace( "CODEC_TYPE_", "" ) );
244
245            // Get bitrate if available
246            if let Some( bit_rate ) = track.codec_params.bits_per_sample {
247                // Calculate approximate bitrate: bits_per_sample * sample_rate * channels
248                let bitrate = bit_rate * self.sample_rate * self.channels as u32 / 1000;
249                meta.bitrate = Some( bitrate );
250            }
251        }
252
253        meta
254    }
255
256
257    /// Returns the signal specification.
258    pub fn signal_spec( &self ) -> SignalSpec {
259        SignalSpec::new( self.sample_rate, symphonia::core::audio::Channels::FRONT_LEFT | symphonia::core::audio::Channels::FRONT_RIGHT )
260    }
261
262
263    /// Decodes the next packet and returns interleaved f32 samples.
264    ///
265    /// Returns None when EOF is reached.
266    pub fn decode_next( &mut self ) -> Result<Option<Vec<f32>>, DecoderError> {
267        loop {
268            // Get the next packet
269            let packet = match self.format_reader.next_packet() {
270                Ok( packet ) => packet,
271                Err( symphonia::core::errors::Error::IoError( ref e ) )
272                    if e.kind() == std::io::ErrorKind::UnexpectedEof =>
273                {
274                    return Ok( None ); // EOF
275                }
276                Err( e ) => {
277                    return Err( DecoderError::Decode( e.to_string() ) );
278                }
279            };
280
281            // Skip packets not for our track
282            if packet.track_id() != self.track_id {
283                continue;
284            }
285
286            // Decode the packet
287            let decoded = match self.decoder.decode( &packet ) {
288                Ok( decoded ) => decoded,
289                Err( symphonia::core::errors::Error::DecodeError( _ ) ) => {
290                    // Decode errors are recoverable, skip this packet
291                    continue;
292                }
293                Err( e ) => {
294                    return Err( DecoderError::Decode( e.to_string() ) );
295                }
296            };
297
298            // Convert to f32 samples
299            let spec = *decoded.spec();
300            let num_frames = decoded.frames();
301
302            // Create or resize sample buffer
303            if self.sample_buf.is_none() || self.sample_buf.as_ref().unwrap().capacity() < num_frames {
304                self.sample_buf = Some( SampleBuffer::new( num_frames as u64, spec ) );
305            }
306
307            let sample_buf = self.sample_buf.as_mut().unwrap();
308            sample_buf.copy_interleaved_ref( decoded );
309
310            return Ok( Some( sample_buf.samples().to_vec() ) );
311        }
312    }
313
314
315    /// Seeks to a position in seconds.
316    pub fn seek( &mut self, position_secs: f64 ) -> Result<(), DecoderError> {
317        let seek_to = SeekTo::Time {
318            time: Time::from( position_secs ),
319            track_id: Some( self.track_id ),
320        };
321
322        self.format_reader
323            .seek( SeekMode::Accurate, seek_to )
324            .map_err( |e| DecoderError::Seek( e.to_string() ) )?;
325
326        // Reset decoder state after seek
327        self.decoder.reset();
328
329        Ok(())
330    }
331}