Skip to main content

bliss_audio/song/
decoder.rs

1//! Module holding all the nitty-gritty decoding details.
2//!
3//! Contains the generic code used to interface the decoding of songs. The [ffmpeg]
4//! submodule contains the code to decode songs with [FFmpeg](https://www.ffmpeg.org/),
5//! while [symphonia] contains the code to decode songs with
6//! [Symphonia](https://github.com/pdeljanov/symphonia).
7//!
8//! Also holds the `Decoder` trait, that you can use to decode songs
9//! with the ffmpeg or symphonia struct that implements that trait,
10//! or implement it for other decoders (GStreamer...).
11//! Using the [ffmpeg] or [symphonia] structs as references
12//! to implement other decoders is a good starting point.
13use log::info;
14
15use crate::{cue::BlissCue, song::AnalysisOptions, BlissError, BlissResult, Song};
16use std::{
17    num::NonZeroUsize,
18    path::{Path, PathBuf},
19    sync::mpsc,
20    thread,
21    time::Duration,
22};
23
24#[derive(Default, Debug)]
25/// A struct used to represent a song that has been decoded, but not analyzed yet.
26///
27/// Most users will not need to use it, as most users won't implement
28/// their decoders, but rely on `ffmpeg` to decode songs, and use `FFmpegDecoder::song_from_path`.
29///
30/// Since it contains the fully decoded song inside of
31/// `PreAnalyzedSong::sample_array`, it can be very large. Users should
32/// convert it to a `Song` as soon as possible, since it is this
33/// structure's only reason to be.
34pub struct PreAnalyzedSong {
35    /// Song's provided file path
36    pub path: PathBuf,
37    /// Song's artist, read from the metadata
38    pub artist: Option<String>,
39    /// Song's album's artist name, read from the metadata
40    pub album_artist: Option<String>,
41    /// Song's title, read from the metadata
42    pub title: Option<String>,
43    /// Song's album name, read from the metadata
44    pub album: Option<String>,
45    /// Song's tracked number, read from the metadata
46    pub track_number: Option<i32>,
47    /// Song's disc number, read from the metadata
48    pub disc_number: Option<i32>,
49    /// Song's genre, read from the metadata
50    pub genre: Option<String>,
51    /// The song's duration
52    pub duration: Duration,
53    /// An array of the song's decoded sample which should be,
54    /// prior to analysis, resampled to f32le, one channel, with a sampling rate
55    /// of 22050 Hz. Anything other than that will yield wrong results.
56    /// To double-check that your sample array has the right format, you could run
57    /// `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le -f hash -hash addler32 -`,
58    /// which will give you the addler32 checksum of the sample array if the song
59    /// has been decoded properly. You can then compute the addler32 checksum of your sample
60    /// array (see `_test_decode` in the tests) and make sure both are the same.
61    ///
62    /// (Running `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le` will simply give
63    /// you the raw sample array as it should look like, if you're not into computing checksums)
64    pub sample_array: Vec<f32>,
65}
66
67#[cfg(feature = "ffmpeg")]
68/// Decoder that uses ffmpeg by default. Uses the `symphonia` feature
69/// without ffmpeg to use symphonia instead.
70pub type DefaultDecoder = ffmpeg::FFmpegDecoder;
71
72#[cfg(all(not(feature = "ffmpeg"), feature = "symphonia"))]
73/// Decoder that uses symphonia.
74pub type DefaultDecoder = symphonia::SymphoniaDecoder;
75
76impl TryFrom<PreAnalyzedSong> for Song {
77    type Error = BlissError;
78
79    fn try_from(raw_song: PreAnalyzedSong) -> BlissResult<Song> {
80        raw_song.to_song_with_options(AnalysisOptions::default())
81    }
82}
83
84impl PreAnalyzedSong {
85    fn to_song_with_options(&self, analysis_options: AnalysisOptions) -> BlissResult<Song> {
86        Ok(Song {
87            path: self.path.clone(),
88            artist: self.artist.clone(),
89            album_artist: self.album_artist.clone(),
90            title: self.title.clone(),
91            album: self.album.clone(),
92            track_number: self.track_number,
93            disc_number: self.disc_number,
94            genre: self.genre.clone(),
95            duration: self.duration,
96            analysis: Song::analyze_with_options(&self.sample_array, &analysis_options)?,
97            features_version: analysis_options.features_version,
98            cue_info: None,
99        })
100    }
101}
102
103/// Trait used to implement your own decoder.
104///
105/// The `decode` function should be implemented so that it
106/// decodes and resample a song to one channel with a sampling rate of 22050 Hz
107/// and a f32le layout.
108/// Once it is implemented, several functions
109/// to perform analysis from path(s) are available, such as
110/// [song_from_path](Decoder::song_from_path) and
111/// [analyze_paths](Decoder::analyze_paths).
112///
113/// For a reference on how to implement that trait, look at the
114/// [FFmpeg](ffmpeg::FFmpegDecoder) decoder
115pub trait Decoder {
116    /// A function that should decode and resample a song, optionally
117    /// extracting the song's metadata such as the artist, the album, etc.
118    ///
119    /// The output sample array should be resampled to f32le, one channel, with a sampling rate
120    /// of 22050 Hz. Anything other than that will yield wrong results.
121    /// To double-check that your sample array has the right format, you could run
122    /// `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le -f hash -hash addler32 -`,
123    /// which will give you the addler32 checksum of the sample array if the song
124    /// has been decoded properly. You can then compute the addler32 checksum of your sample
125    /// array (see `_test_decode` in the tests) and make sure both are the same.
126    ///
127    /// (Running `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le` will simply give
128    /// you the raw sample array as it should look like, if you're not into computing checksums)
129    fn decode(path: &Path) -> BlissResult<PreAnalyzedSong>;
130
131    /// Returns a decoded [Song] given a file path, or an error if the song
132    /// could not be analyzed for some reason.
133    ///
134    /// # Arguments
135    ///
136    /// * `path` - A [Path] holding a valid file path to a valid audio file.
137    ///
138    /// # Errors
139    ///
140    /// This function will return an error if the file path is invalid, if
141    /// the file path points to a file containing no or corrupted audio stream,
142    /// or if the analysis could not be conducted to the end for some reason.
143    ///
144    /// The error type returned should give a hint as to whether it was a
145    /// decoding ([DecodingError](BlissError::DecodingError)) or an analysis
146    /// ([AnalysisError](BlissError::AnalysisError)) error.
147    fn song_from_path<P: AsRef<Path>>(path: P) -> BlissResult<Song> {
148        Self::decode(path.as_ref())?.try_into()
149    }
150
151    /// Returns a decoded [Song] given a file path, processed with the options
152    /// `analysis_options` or an error if the song could not be analyzed for some
153    /// reason. Use this if you want to analyze a song with older features version.
154    ///
155    /// # Arguments
156    ///
157    /// * `path` - A [Path] holding a valid file path to a valid audio file.
158    /// * `analysis_options`: An [AnalysisOptions] struct holding various
159    ///   analysis options, such as the feature version. The `number_cores`
160    ///   parameter is not used here, since only a single song is processed.
161    ///
162    /// # Errors
163    ///
164    /// This function will return an error if the file path is invalid, if
165    /// the file path points to a file containing no or corrupted audio stream,
166    /// or if the analysis could not be conducted to the end for some reason.
167    ///
168    /// The error type returned should give a hint as to whether it was a
169    /// decoding ([DecodingError](BlissError::DecodingError)) or an analysis
170    /// ([AnalysisError](BlissError::AnalysisError)) error.
171    fn song_from_path_with_options<P: AsRef<Path>>(
172        path: P,
173        analysis_options: AnalysisOptions,
174    ) -> BlissResult<Song> {
175        Self::decode(path.as_ref())?.to_song_with_options(analysis_options)
176    }
177
178    /// Analyze songs in `paths` using multiple threads, and return the
179    /// analyzed [Song] objects through an [mpsc::IntoIter].
180    ///
181    /// Returns an iterator, whose items are a tuple made of
182    /// the song path (to display to the user in case the analysis failed),
183    /// and a `Result<Song>`.
184    ///
185    /// # Note
186    ///
187    /// This function also works with CUE files - it finds the audio files
188    /// mentionned in the CUE sheet, and then runs the analysis on each song
189    /// defined by it, returning a proper [Song] object for each one of them.
190    ///
191    /// Make sure that you don't submit both the audio file along with the CUE
192    /// sheet if your library uses them, otherwise the audio file will be
193    /// analyzed as one, single, long song. For instance, with a CUE sheet named
194    /// `cue-file.cue` with the corresponding audio files `album-1.wav` and
195    /// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue`
196    /// to `analyze_paths`, and it will return [Song]s from both files, with
197    /// more information about which file it is extracted from in the
198    /// [cue info field](Song::cue_info).
199    ///
200    /// This example uses FFmpeg to decode songs by default, but it is possible to
201    /// implement another decoder and replace `use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;`
202    /// by a custom decoder.
203    ///
204    #[cfg_attr(
205        feature = "ffmpeg",
206        doc = r##"
207# Example
208
209```no_run
210use bliss_audio::{BlissResult};
211use bliss_audio::decoder::Decoder as DecoderTrait;
212use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
213
214fn main() -> BlissResult<()> {
215    let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
216    for (path, result) in Decoder::analyze_paths(&paths) {
217        match result {
218            Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
219            Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
220        }
221    }
222    Ok(())
223}
224```"##
225    )]
226    fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
227        paths: F,
228    ) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
229        Self::analyze_paths_with_options(paths, AnalysisOptions::default())
230    }
231
232    /// Analyze songs in `paths`, and return the analyzed [Song] objects through an
233    /// [mpsc::IntoIter]. `number_cores` sets the number of cores the analysis
234    /// will use, capped by your system's capacity. Most of the time, you want to
235    /// use the simpler `analyze_paths` functions, which autodetects the number
236    /// of cores in your system.
237    ///
238    /// Return an iterator, whose items are a tuple made of
239    /// the song path (to display to the user in case the analysis failed),
240    /// and a `Result<Song>`.
241    ///
242    /// # Note
243    ///
244    /// This function also works with CUE files - it finds the audio files
245    /// mentionned in the CUE sheet, and then runs the analysis on each song
246    /// defined by it, returning a proper [Song] object for each one of them.
247    ///
248    /// Make sure that you don't submit both the audio file along with the CUE
249    /// sheet if your library uses them, otherwise the audio file will be
250    /// analyzed as one, single, long song. For instance, with a CUE sheet named
251    /// `cue-file.cue` with the corresponding audio files `album-1.wav` and
252    /// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue`
253    /// to `analyze_paths`, and it will return [Song]s from both files, with
254    /// more information about which file it is extracted from in the
255    /// [cue info field](Song::cue_info).
256    #[cfg_attr(
257        feature = "ffmpeg",
258        doc = r##"
259# Example
260
261```no_run
262use bliss_audio::BlissResult;
263use bliss_audio::decoder::Decoder as DecoderTrait;
264use bliss_audio::decoder::ffmpeg::FFmpegDecoder as Decoder;
265
266fn main() -> BlissResult<()> {
267    let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
268    for (path, result) in Decoder::analyze_paths(&paths) {
269        match result {
270            Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
271            Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
272        }
273    }
274    Ok(())
275}
276```"##
277    )]
278    fn analyze_paths_with_options<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
279        paths: F,
280        analysis_options: AnalysisOptions,
281    ) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
282        let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap());
283        let desired_number_cores = analysis_options.number_cores;
284        // If the number of cores that we have is greater than the number of cores
285        // that the user asked, comply with the user - otherwise we set a number
286        // that's too great.
287        if cores > desired_number_cores {
288            cores = desired_number_cores;
289        }
290        let paths: Vec<PathBuf> = paths.into_iter().map(|p| p.into()).collect();
291        #[allow(clippy::type_complexity)]
292        let (tx, rx): (
293            mpsc::Sender<(PathBuf, BlissResult<Song>)>,
294            mpsc::Receiver<(PathBuf, BlissResult<Song>)>,
295        ) = mpsc::channel();
296        if paths.is_empty() {
297            return rx.into_iter();
298        }
299        let mut handles = Vec::new();
300        let mut chunk_length = paths.len() / cores;
301        if chunk_length == 0 {
302            chunk_length = paths.len();
303        }
304        for chunk in paths.chunks(chunk_length) {
305            let tx_thread = tx.clone();
306            let owned_chunk = chunk.to_owned();
307            let child = thread::spawn(move || {
308                for path in owned_chunk {
309                    info!("Analyzing file '{path:?}'");
310                    if let Some(extension) = Path::new(&path).extension() {
311                        let extension = extension.to_string_lossy().to_lowercase();
312                        if extension == "cue" {
313                            match BlissCue::<Self>::songs_from_path(&path) {
314                                Ok(songs) => {
315                                    for song in songs {
316                                        tx_thread.send((path.to_owned(), song)).unwrap();
317                                    }
318                                }
319                                Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(),
320                            };
321                            continue;
322                        }
323                    }
324                    let song = Self::song_from_path_with_options(&path, analysis_options);
325                    tx_thread.send((path.to_owned(), song)).unwrap();
326                }
327            });
328            handles.push(child);
329        }
330
331        rx.into_iter()
332    }
333}
334
335#[cfg(feature = "symphonia")]
336pub mod symphonia;
337
338#[cfg(feature = "ffmpeg")]
339pub mod ffmpeg;