Add analyze_paths_with_core, return proper path
This commit is contained in:
parent
701feb414d
commit
8d0d77da7d
7 changed files with 145 additions and 77 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
200
src/lib.rs
200
src/lib.rs
|
@ -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,
|
||||||
(
|
PathBuf::from("./data/testcue.cue"),
|
||||||
false,
|
Some(String::from(
|
||||||
String::from("./data/testcue.cue"),
|
"error happened while decoding file – while \
|
||||||
Some(String::from(
|
|
||||||
"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue