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;