Add analyze_paths_with_core, return proper path

This commit is contained in:
Polochon-street 2022-09-22 19:13:27 +02:00
parent 701feb414d
commit 8d0d77da7d
7 changed files with 145 additions and 77 deletions

View File

@ -1,8 +1,12 @@
#Changelog #Changelog
## bliss 5.2.3 ## bliss 0.6.0
* Fix a bug with some broken MP3 files * Change String to PathBuf in `analyze_paths`.
* Bump ffmpeg to 5.1.0 * Add `analyze_paths_with_cores`.
## bliss 0.5.2
* Fix a bug with some broken MP3 files.
* Bump ffmpeg to 5.1.0.
## bliss 0.5.0 ## bliss 0.5.0
* Add support for CUE files. * Add support for CUE files.

2
Cargo.lock generated
View File

@ -85,7 +85,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bliss-audio" name = "bliss-audio"
version = "0.5.3" version = "0.6.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bliss-audio-aubio-rs", "bliss-audio-aubio-rs",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "bliss-audio" name = "bliss-audio"
version = "0.5.3" version = "0.6.0"
authors = ["Polochon-street <polochonstreet@gmx.fr>"] authors = ["Polochon-street <polochonstreet@gmx.fr>"]
edition = "2018" edition = "2018"
license = "GPL-3.0-only" license = "GPL-3.0-only"

View File

@ -78,7 +78,7 @@ fn main() -> Result<()> {
for (path, result) in song_iterator { for (path, result) in song_iterator {
match result { match result {
Ok(song) => analyzed_songs.push(song), Ok(song) => analyzed_songs.push(song),
Err(e) => println!("error analyzing {}: {}", path, e), Err(e) => println!("error analyzing {}: {}", path.display(), e),
}; };
} }
analyzed_songs.extend_from_slice(&songs); analyzed_songs.extend_from_slice(&songs);

View File

@ -180,7 +180,7 @@ fn chroma_filter(
}), }),
); );
let mut d: Array2<f64> = Array::zeros((n_chroma as usize, (&freq_bins).len())); let mut d: Array2<f64> = Array::zeros((n_chroma as usize, (freq_bins).len()));
for (idx, mut row) in d.rows_mut().into_iter().enumerate() { for (idx, mut row) in d.rows_mut().into_iter().enumerate() {
row.fill(idx as f64); row.fill(idx as f64);
} }
@ -207,7 +207,7 @@ fn chroma_filter(
wts *= &freq_bins; wts *= &freq_bins;
// np.roll(), np bro // np.roll(), np bro
let mut uninit: Vec<f64> = vec![0.; (&wts).len()]; let mut uninit: Vec<f64> = vec![0.; (wts).len()];
unsafe { unsafe {
uninit.set_len(wts.len()); uninit.set_len(wts.len());
} }

View File

@ -150,7 +150,7 @@ impl BlissCueFile {
// located using the sample_array and the timestamp delimiter. // located using the sample_array and the timestamp delimiter.
fn get_songs(&self) -> Vec<BlissResult<Song>> { fn get_songs(&self) -> Vec<BlissResult<Song>> {
let mut songs = Vec::new(); let mut songs = Vec::new();
for (index, tuple) in (&self.tracks[..]).windows(2).enumerate() { for (index, tuple) in (self.tracks[..]).windows(2).enumerate() {
let (current_track, next_track) = (tuple[0].to_owned(), tuple[1].to_owned()); let (current_track, next_track) = (tuple[0].to_owned(), tuple[1].to_owned());
if let Some((_, start_current)) = current_track.indices.get(0) { if let Some((_, start_current)) = current_track.indices.get(0) {
if let Some((_, end_current)) = next_track.indices.get(0) { if let Some((_, end_current)) = next_track.indices.get(0) {

View File

@ -32,26 +32,25 @@
//! //!
//! ### Make a playlist from a song, discarding failed songs //! ### Make a playlist from a song, discarding failed songs
//! ```no_run //! ```no_run
//! use bliss_audio::{analyze_paths, BlissResult, Song}; //! use bliss_audio::{
//! use noisy_float::prelude::n32; //! analyze_paths,
//! playlist::{closest_to_first_song, euclidean_distance},
//! BlissResult, Song,
//! };
//! //!
//! fn main() -> BlissResult<()> { //! fn main() -> BlissResult<()> {
//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"]; //! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
//! let mut songs: Vec<Song> = analyze_paths(&paths) //! let mut songs: Vec<Song> = analyze_paths(&paths).filter_map(|(_, s)| s.ok()).collect();
//! .filter_map(|(_, s)| s.ok())
//! .collect();
//! //!
//! // Assuming there is a first song //! // Assuming there is a first song
//! let first_song = songs.first().unwrap().to_owned(); //! let first_song = songs.first().unwrap().to_owned();
//! //!
//! songs.sort_by_cached_key(|song| n32(first_song.distance(&song))); //! closest_to_first_song(&first_song, &mut songs, euclidean_distance);
//! println!( //!
//! "Playlist is: {:?}", //! println!("Playlist is:");
//! songs //! for song in songs {
//! .iter() //! println!("{}", song.path.display());
//! .map(|song| song.path.to_string_lossy().to_string()) //! }
//! .collect::<Vec<String>>()
//! );
//! Ok(()) //! Ok(())
//! } //! }
//! ``` //! ```
@ -137,7 +136,7 @@ pub type BlissResult<T> = Result<T, BlissError>;
/// for (path, result) in analyze_paths(&paths) { /// for (path, result) in analyze_paths(&paths) {
/// match result { /// match result {
/// Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title), /// Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
/// Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path, e), /// Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
/// } /// }
/// } /// }
/// Ok(()) /// Ok(())
@ -145,19 +144,70 @@ pub type BlissResult<T> = Result<T, BlissError>;
/// ``` /// ```
pub fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>( pub fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
paths: F, paths: F,
) -> mpsc::IntoIter<(String, BlissResult<Song>)> { ) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
let num_cpus = num_cpus::get(); let cores = num_cpus::get();
analyze_paths_with_cores(paths, cores)
}
/// Analyze songs in `paths`, and return the analyzed [Song] objects through an
/// [mpsc::IntoIter]. `number_cores` sets the number of cores the analysis
/// will use, capped by your system's capacity. Most of the time, you want to
/// use the simpler `analyze_paths` functions, which autodetects the number
/// of cores in your system.
///
/// Return an iterator, whose items are a tuple made of
/// the song path (to display to the user in case the analysis failed),
/// and a Result<Song>.
///
/// # Note
///
/// This function also works with CUE files - it finds the audio files
/// mentionned in the CUE sheet, and then runs the analysis on each song
/// defined by it, returning a proper [Song] object for each one of them.
///
/// Make sure that you don't submit both the audio file along with the CUE
/// sheet if your library uses them, otherwise the audio file will be
/// analyzed as one, single, long song. For instance, with a CUE sheet named
/// `cue-file.cue` with the corresponding audio files `album-1.wav` and
/// `album-2.wav` defined in the CUE sheet, you would just pass `cue-file.cue`
/// to `analyze_paths`, and it will return [Song]s from both files, with
/// more information about which file it is extracted from in the
/// [cue info field](Song::cue_info).
///
/// # Example:
/// ```no_run
/// use bliss_audio::{analyze_paths_with_cores, BlissResult};
///
/// fn main() -> BlissResult<()> {
/// let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")];
/// for (path, result) in analyze_paths_with_cores(&paths, 2) {
/// match result {
/// Ok(song) => println!("Do something with analyzed song {} with title {:?}", song.path.display(), song.title),
/// Err(e) => println!("Song at {} could not be analyzed. Failed with: {}", path.display(), e),
/// }
/// }
/// Ok(())
/// }
/// ```
pub fn analyze_paths_with_cores<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
paths: F,
number_cores: usize,
) -> mpsc::IntoIter<(PathBuf, BlissResult<Song>)> {
let mut cores = num_cpus::get();
if cores > number_cores {
cores = number_cores;
}
let paths: Vec<PathBuf> = paths.into_iter().map(|p| p.into()).collect(); let paths: Vec<PathBuf> = paths.into_iter().map(|p| p.into()).collect();
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
let (tx, rx): ( let (tx, rx): (
mpsc::Sender<(String, BlissResult<Song>)>, mpsc::Sender<(PathBuf, BlissResult<Song>)>,
mpsc::Receiver<(String, BlissResult<Song>)>, mpsc::Receiver<(PathBuf, BlissResult<Song>)>,
) = mpsc::channel(); ) = mpsc::channel();
if paths.is_empty() { if paths.is_empty() {
return rx.into_iter(); return rx.into_iter();
} }
let mut handles = Vec::new(); let mut handles = Vec::new();
let mut chunk_length = paths.len() / num_cpus; let mut chunk_length = paths.len() / cores;
if chunk_length == 0 { if chunk_length == 0 {
chunk_length = paths.len(); chunk_length = paths.len();
} }
@ -173,22 +223,16 @@ pub fn analyze_paths<P: Into<PathBuf>, F: IntoIterator<Item = P>>(
match BlissCue::songs_from_path(&path) { match BlissCue::songs_from_path(&path) {
Ok(songs) => { Ok(songs) => {
for song in songs { for song in songs {
tx_thread tx_thread.send((path.to_owned(), song)).unwrap();
.send((path.to_string_lossy().to_string(), song))
.unwrap();
} }
} }
Err(e) => tx_thread Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(),
.send((path.to_string_lossy().to_string(), Err(e)))
.unwrap(),
}; };
continue; continue;
} }
} }
let song = Song::from_path(&path); let song = Song::from_path(&path);
tx_thread tx_thread.send((path.to_owned(), song)).unwrap();
.send((path.to_string_lossy().to_string(), song))
.unwrap();
} }
}); });
handles.push(child); handles.push(child);
@ -218,55 +262,75 @@ mod tests {
#[test] #[test]
fn test_analyze_paths() { fn test_analyze_paths() {
let paths = vec![ let paths = vec![
String::from("./data/s16_mono_22_5kHz.flac"), PathBuf::from("./data/s16_mono_22_5kHz.flac"),
String::from("./data/testcue.cue"), PathBuf::from("./data/testcue.cue"),
String::from("./data/white_noise.flac"), PathBuf::from("./data/white_noise.flac"),
String::from("definitely-not-existing.foo"), PathBuf::from("definitely-not-existing.foo"),
String::from("not-existing.foo"), PathBuf::from("not-existing.foo"),
]; ];
let mut results = analyze_paths(&paths) let mut results = analyze_paths(&paths)
.map(|x| match &x.1 { .map(|x| match &x.1 {
Ok(s) => (true, s.path.to_string_lossy().to_string(), None), Ok(s) => (true, s.path.to_owned(), None),
Err(e) => (false, x.0.to_owned(), Some(e.to_string())), Err(e) => (false, x.0.to_owned(), Some(e.to_string())),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
results.sort(); results.sort();
assert_eq!( let expected_results = vec![
results,
vec![
( (
false, false,
String::from("./data/testcue.cue"), PathBuf::from("./data/testcue.cue"),
Some(String::from( Some(String::from(
"error happened while decoding file while \ "error happened while decoding file while \
opening format for file './data/not-existing.wav': \ opening format for file './data/not-existing.wav': \
ffmpeg::Error(2: No such file or directory)." ffmpeg::Error(2: No such file or directory).",
),), )),
), ),
( (
false, false,
String::from("definitely-not-existing.foo"), PathBuf::from("definitely-not-existing.foo"),
Some(String::from( Some(String::from(
"error happened while decoding file while \ "error happened while decoding file while \
opening format for file 'definitely-not-existing\ opening format for file 'definitely-not-existing\
.foo': ffmpeg::Error(2: No such file or directory)." .foo': ffmpeg::Error(2: No such file or directory).",
),), )),
), ),
( (
false, false,
String::from("not-existing.foo"), PathBuf::from("not-existing.foo"),
Some(String::from( Some(String::from(
"error happened while decoding file \ "error happened while decoding file \
while opening format for file 'not-existing.foo': \ while opening format for file 'not-existing.foo': \
ffmpeg::Error(2: No such file or directory)." ffmpeg::Error(2: No such file or directory).",
),), )),
), ),
(true, String::from("./data/s16_mono_22_5kHz.flac"), None), (true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None),
(true, String::from("./data/testcue.flac/CUE_TRACK001"), None), (
(true, String::from("./data/testcue.flac/CUE_TRACK002"), None), true,
(true, String::from("./data/testcue.flac/CUE_TRACK003"), None), PathBuf::from("./data/testcue.flac/CUE_TRACK001"),
(true, String::from("./data/white_noise.flac"), None), None,
], ),
); (
true,
PathBuf::from("./data/testcue.flac/CUE_TRACK002"),
None,
),
(
true,
PathBuf::from("./data/testcue.flac/CUE_TRACK003"),
None,
),
(true, PathBuf::from("./data/white_noise.flac"), None),
];
assert_eq!(results, expected_results);
let mut results = analyze_paths_with_cores(&paths, 1)
.map(|x| match &x.1 {
Ok(s) => (true, s.path.to_owned(), None),
Err(e) => (false, x.0.to_owned(), Some(e.to_string())),
})
.collect::<Vec<_>>();
results.sort();
assert_eq!(results, expected_results);
} }
} }