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;