bliss_audio/song/
decoder.rs

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