From 1b39567b310d67b4676c8c60e2219155f13af596 Mon Sep 17 00:00:00 2001 From: Polochon_street Date: Mon, 1 Apr 2024 21:25:47 +0200 Subject: [PATCH] Continue the optional ffmpeg feature --- .github/workflows/rust.yml | 3 + TODO.md | 1 + ci_check.sh | 2 +- examples/analyze.rs | 6 +- examples/distance.rs | 9 +- examples/library.rs | 7 +- examples/library_extra_info.rs | 7 +- examples/playlist.rs | 8 +- src/chroma.rs | 21 +- src/cue.rs | 29 +- src/lib.rs | 220 +------ src/library.rs | 70 ++- src/misc.rs | 6 +- src/song.rs | 1025 ++++++++++++++++++++------------ src/temporal.rs | 7 +- src/timbral.rs | 17 +- src/utils.rs | 11 +- 17 files changed, 761 insertions(+), 688 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0de5402..aa1dcad 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,6 +40,9 @@ jobs: run: cargo +nightly-2024-01-16 bench --verbose --features=bench --no-run - name: Build examples run: cargo build --examples --verbose --features=serde,library + - name: Build without ffmpeg + run: cargo build --verbose --no-default-features + build-test-lint-windows: name: Windows - build, test and lint diff --git a/TODO.md b/TODO.md index 2632c3b..facca22 100644 --- a/TODO.md +++ b/TODO.md @@ -12,6 +12,7 @@ ask questions if you want to tackle an item. - Split out ffmpeg (see https://github.com/Polochon-street/bliss-rs/issues/63 and https://users.rust-lang.org/t/proper-way-to-abstract-a-third-party-provider/107076/8) - Make ffmpeg an optional (but default) feature + - The library trait must be Decoder-agnostic, and not depend on FFmpeg - Make the tests that don't need it not dependent of ffmpeg - Make the Song::from_path a trait that is by default implemented with the ffmpeg feature (so you can theoretically implement the library trait without ffmpeg) diff --git a/ci_check.sh b/ci_check.sh index 5b0011f..d251095 100755 --- a/ci_check.sh +++ b/ci_check.sh @@ -1 +1 @@ -cargo fmt -- --check && cargo clippy --examples --features=serde -- -D warnings && cargo build --verbose && cargo test --verbose && cargo test --verbose --examples && cargo +nightly-2023-02-16 bench --verbose --features=bench --no-run && cargo build --examples --verbose --features=serde +cargo fmt -- --check && cargo clippy --examples --features=serde -- -D warnings && cargo build --verbose && cargo test --verbose && cargo test --verbose --examples && cargo +nightly-2024-01-16 bench --verbose --features=bench --no-run && cargo build --examples --verbose --features=serde && cargo build --no-default-features diff --git a/examples/analyze.rs b/examples/analyze.rs index 2ffab60..d01ea9a 100644 --- a/examples/analyze.rs +++ b/examples/analyze.rs @@ -1,4 +1,6 @@ -use bliss_audio::Song; +#[cfg(feature = "ffmpeg")] +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; +use bliss_audio::decoder::Decoder as DecoderTrait; use std::env; /** @@ -9,7 +11,7 @@ use std::env; fn main() { let args: Vec = env::args().skip(1).collect(); for path in &args { - match Song::from_path(path) { + match Decoder::song_from_path(path) { Ok(song) => println!("{}: {:?}", path, song.analysis), Err(e) => println!("{path}: {e}"), } diff --git a/examples/distance.rs b/examples/distance.rs index 870eacd..d8e6fb9 100644 --- a/examples/distance.rs +++ b/examples/distance.rs @@ -1,4 +1,7 @@ -use bliss_audio::{playlist::euclidean_distance, Song}; +#[cfg(feature = "ffmpeg")] +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; +use bliss_audio::decoder::Decoder as DecoderTrait; +use bliss_audio::playlist::euclidean_distance; use std::env; /** @@ -13,8 +16,8 @@ fn main() -> Result<(), String> { let first_path = paths.next().ok_or("Help: ./distance ")?; let second_path = paths.next().ok_or("Help: ./distance ")?; - let song1 = Song::from_path(first_path).map_err(|x| x.to_string())?; - let song2 = Song::from_path(second_path).map_err(|x| x.to_string())?; + let song1 = Decoder::song_from_path(first_path).map_err(|x| x.to_string())?; + let song2 = Decoder::song_from_path(second_path).map_err(|x| x.to_string())?; println!( "d({:?}, {:?}) = {}", diff --git a/examples/library.rs b/examples/library.rs index 1cabb09..3a55e38 100644 --- a/examples/library.rs +++ b/examples/library.rs @@ -4,6 +4,7 @@ /// For simplicity's sake, this example recursively gets songs from a folder /// to emulate an audio player library, without handling CUE files. use anyhow::Result; +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; use bliss_audio::library::{AppConfigTrait, BaseConfig, Library}; use clap::{App, Arg, SubCommand}; use glob::glob; @@ -68,7 +69,7 @@ trait CustomLibrary { fn song_paths(&self) -> Result>; } -impl CustomLibrary for Library { +impl CustomLibrary for Library { /// Get all songs in the player library fn song_paths(&self) -> Result> { let music_path = &self.config.music_library_path; @@ -180,7 +181,7 @@ fn main() -> Result<()> { library.analyze_paths(library.song_paths()?, true)?; } else if let Some(sub_m) = matches.subcommand_matches("update") { let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let mut library: Library = Library::from_config_path(config_path)?; + let mut library: Library = Library::from_config_path(config_path)?; library.update_library(library.song_paths()?, true, true)?; } else if let Some(sub_m) = matches.subcommand_matches("playlist") { let song_path = sub_m.value_of("SONG_PATH").unwrap(); @@ -189,7 +190,7 @@ fn main() -> Result<()> { .value_of("playlist-length") .unwrap_or("20") .parse::()?; - let library: Library = Library::from_config_path(config_path)?; + let library: Library = Library::from_config_path(config_path)?; let songs = library.playlist_from::<()>(&[song_path], playlist_length)?; let song_paths = songs .into_iter() diff --git a/examples/library_extra_info.rs b/examples/library_extra_info.rs index a0d3167..d53a939 100644 --- a/examples/library_extra_info.rs +++ b/examples/library_extra_info.rs @@ -5,6 +5,7 @@ /// For simplicity's sake, this example recursively gets songs from a folder /// to emulate an audio player library, without handling CUE files. use anyhow::Result; +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; use bliss_audio::library::{AppConfigTrait, BaseConfig, Library}; use clap::{App, Arg, SubCommand}; use glob::glob; @@ -69,7 +70,7 @@ trait CustomLibrary { fn song_paths_info(&self) -> Result>; } -impl CustomLibrary for Library { +impl CustomLibrary for Library { /// Get all songs in the player library, along with the extra info /// one would want to store along with each song. fn song_paths_info(&self) -> Result> { @@ -198,7 +199,7 @@ fn main() -> Result<()> { library.analyze_paths_extra_info(library.song_paths_info()?, true)?; } else if let Some(sub_m) = matches.subcommand_matches("update") { let config_path = sub_m.value_of("config-path").map(PathBuf::from); - let mut library: Library = Library::from_config_path(config_path)?; + let mut library: Library = Library::from_config_path(config_path)?; library.update_library_extra_info(library.song_paths_info()?, true, true)?; } else if let Some(sub_m) = matches.subcommand_matches("playlist") { let song_path = sub_m.value_of("SONG_PATH").unwrap(); @@ -207,7 +208,7 @@ fn main() -> Result<()> { .value_of("playlist-length") .unwrap_or("20") .parse::()?; - let library: Library = Library::from_config_path(config_path)?; + let library: Library = Library::from_config_path(config_path)?; let songs = library.playlist_from::(&[song_path], playlist_length)?; let playlist = songs .into_iter() diff --git a/examples/playlist.rs b/examples/playlist.rs index c106937..ab7803a 100644 --- a/examples/playlist.rs +++ b/examples/playlist.rs @@ -1,6 +1,8 @@ use anyhow::Result; +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; +use bliss_audio::decoder::Decoder as DecoderTrait; use bliss_audio::playlist::{closest_to_songs, dedup_playlist, euclidean_distance}; -use bliss_audio::{analyze_paths, Song}; +use bliss_audio::Song; use clap::{App, Arg}; use glob::glob; use std::env; @@ -56,14 +58,14 @@ fn main() -> Result<()> { .map(|x| x.to_string_lossy().to_string()) .collect::>(); - let song_iterator = analyze_paths( + let song_iterator = Decoder::analyze_paths( paths .iter() .filter(|p| !analyzed_paths.contains(&PathBuf::from(p))) .map(|p| p.to_owned()) .collect::>(), ); - let first_song = Song::from_path(file)?; + let first_song = Decoder::song_from_path(file)?; let mut analyzed_songs = vec![first_song.to_owned()]; for (path, result) in song_iterator { match result { diff --git a/src/chroma.rs b/src/chroma.rs index 37045ee..fbdc4a0 100644 --- a/src/chroma.rs +++ b/src/chroma.rs @@ -365,9 +365,12 @@ fn chroma_stft( mod test { use super::*; #[cfg(feature = "ffmpeg")] + use crate::song::decoder::bliss_ffmpeg::FFmpeg as Decoder; + use crate::song::decoder::Decoder as DecoderTrait; + #[cfg(feature = "ffmpeg")] use crate::utils::stft; #[cfg(feature = "ffmpeg")] - use crate::{Song, SAMPLE_RATE}; + use crate::SAMPLE_RATE; use ndarray::{arr1, arr2, Array2}; use ndarray_npy::ReadNpyExt; use std::fs::File; @@ -439,7 +442,7 @@ mod test { #[test] #[cfg(feature = "ffmpeg")] fn test_chroma_desc() { - let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); + let song = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); chroma_desc.do_(&song.sample_array).unwrap(); let expected_values = vec![ @@ -462,7 +465,7 @@ mod test { #[test] #[cfg(feature = "ffmpeg")] fn test_chroma_stft_decode() { - let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")) + let signal = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")) .unwrap() .sample_array; let mut stft = stft(&signal, 8192, 2205); @@ -496,7 +499,7 @@ mod test { #[test] #[cfg(feature = "ffmpeg")] fn test_estimate_tuning_decode() { - let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")) + let signal = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")) .unwrap() .sample_array; let stft = stft(&signal, 8192, 2205); @@ -559,8 +562,10 @@ mod test { mod bench { extern crate test; use super::*; + use crate::song::decoder::bliss_ffmpeg::FFmpeg as Decoder; + use crate::song::decoder::Decoder as DecoderTrait; use crate::utils::stft; - use crate::{Song, SAMPLE_RATE}; + use crate::SAMPLE_RATE; use ndarray::{arr2, Array1, Array2}; use ndarray_npy::ReadNpyExt; use std::fs::File; @@ -614,7 +619,7 @@ mod bench { #[bench] #[cfg(feature = "ffmpeg")] fn bench_chroma_desc(b: &mut Bencher) { - let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); + let song = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); let signal = song.sample_array; b.iter(|| { @@ -626,7 +631,7 @@ mod bench { #[bench] #[cfg(feature = "ffmpeg")] fn bench_chroma_stft(b: &mut Bencher) { - let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); + let song = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12); let signal = song.sample_array; b.iter(|| { @@ -638,7 +643,7 @@ mod bench { #[bench] #[cfg(feature = "ffmpeg")] fn bench_chroma_stft_decode(b: &mut Bencher) { - let signal = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")) + let signal = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")) .unwrap() .sample_array; let mut stft = stft(&signal, 8192, 2205); diff --git a/src/cue.rs b/src/cue.rs index 63ced3b..072a9dc 100644 --- a/src/cue.rs +++ b/src/cue.rs @@ -7,16 +7,13 @@ //! and [CueInfo], which is a struct stored in [Song] to keep track of the CUE information //! the song was extracted from. -#[cfg(feature = "ffmpeg")] +use crate::song::decoder::Decoder as DecoderTrait; use crate::{Analysis, BlissError, BlissResult, Song, FEATURES_VERSION, SAMPLE_RATE}; use rcue::cue::{Cue, Track}; -#[cfg(feature = "ffmpeg")] use rcue::parser::parse_from_file; -#[cfg(feature = "ffmpeg")] use std::path::Path; -use std::path::PathBuf; -#[cfg(feature = "ffmpeg")] use std::time::Duration; +use std::{marker::PhantomData, path::PathBuf}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Default, Debug, PartialEq, Eq, Clone)] @@ -37,9 +34,10 @@ pub struct CueInfo { /// Use either [analyze_paths](crate::analyze_paths) with CUE files or /// [songs_from_path](BlissCue::songs_from_path) to return a list of [Song]s /// from CUE files. -pub struct BlissCue { +pub struct BlissCue { cue: Cue, cue_path: PathBuf, + decoder: PhantomData, } #[allow(missing_docs)] @@ -54,16 +52,15 @@ struct BlissCueFile { audio_file_path: PathBuf, } -impl BlissCue { +impl BlissCue { /// Analyze songs from a CUE file, extracting individual [Song] objects /// for each individual song. /// /// Each returned [Song] has a populated [cue_info](Song::cue_info) object, that can be /// be used to retrieve which CUE sheet was used to extract it, as well /// as the corresponding audio file. - #[cfg(feature = "ffmpeg")] pub fn songs_from_path>(path: P) -> BlissResult>> { - let cue = BlissCue::from_path(&path)?; + let cue: BlissCue = BlissCue::from_path(&path)?; let cue_files = cue.files(); let mut songs = Vec::new(); for cue_file in cue_files.into_iter() { @@ -84,7 +81,6 @@ impl BlissCue { } // Extract a BlissCue from a given path. - #[cfg(feature = "ffmpeg")] fn from_path>(path: P) -> BlissResult { let cue = parse_from_file(&path.as_ref().to_string_lossy(), false).map_err(|e| { BlissError::DecodingError(format!( @@ -93,14 +89,14 @@ impl BlissCue { e )) })?; - Ok(BlissCue { + Ok(Self { cue, cue_path: path.as_ref().to_owned(), + decoder: PhantomData, }) } // List all BlissCueFile from a BlissCue. - #[cfg(feature = "ffmpeg")] fn files(&self) -> Vec> { let mut cue_files = Vec::new(); for cue_file in self.cue.files.iter() { @@ -114,7 +110,7 @@ impl BlissCue { .iter() .find(|(c, _)| c == "GENRE") .map(|(_, v)| v.to_owned()); - let raw_song = Song::decode(Path::new(&audio_file_path)); + let raw_song = D::decode(Path::new(&audio_file_path)); if let Ok(song) = raw_song { let bliss_cue_file = BlissCueFile { sample_array: song.sample_array, @@ -134,7 +130,6 @@ impl BlissCue { } } -#[cfg(feature = "ffmpeg")] impl BlissCueFile { fn create_song( &self, @@ -211,12 +206,14 @@ mod tests { #[cfg(feature = "ffmpeg")] use super::*; #[cfg(feature = "ffmpeg")] + use crate::decoder::bliss_ffmpeg::FFmpeg; + #[cfg(feature = "ffmpeg")] use pretty_assertions::assert_eq; #[test] #[cfg(feature = "ffmpeg")] fn test_empty_cue() { - let songs = BlissCue::songs_from_path("data/empty.cue").unwrap(); + let songs = BlissCue::::songs_from_path("data/empty.cue").unwrap(); let error = songs[0].to_owned().unwrap_err(); assert_eq!( error, @@ -227,7 +224,7 @@ mod tests { #[test] #[cfg(feature = "ffmpeg")] fn test_cue_analysis() { - let songs = BlissCue::songs_from_path("data/testcue.cue").unwrap(); + let songs = BlissCue::::songs_from_path("data/testcue.cue").unwrap(); let expected = vec![ Ok(Song { path: Path::new("data/testcue.cue/CUE_TRACK001").to_path_buf(), diff --git a/src/lib.rs b/src/lib.rs index fcb63fc..1cc28c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,21 +87,10 @@ mod utils; #[cfg(feature = "serde")] #[macro_use] extern crate serde; -#[cfg(feature = "ffmpeg")] -use crate::cue::BlissCue; -#[cfg(feature = "ffmpeg")] -use log::info; -#[cfg(feature = "ffmpeg")] -use std::num::NonZeroUsize; -#[cfg(feature = "ffmpeg")] -use std::path::{Path, PathBuf}; -#[cfg(feature = "ffmpeg")] -use std::sync::mpsc; -#[cfg(feature = "ffmpeg")] -use std::thread; + use thiserror::Error; -pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES}; +pub use song::{decoder, Analysis, AnalysisIndex, Song, NUMBER_FEATURES}; #[cfg(feature = "ffmpeg")] const CHANNELS: u16 = 1; @@ -129,150 +118,9 @@ pub enum BlissError { /// bliss error type pub type BlissResult = Result; -/// Analyze songs in `paths`, and return the analyzed [Song] objects through an -/// [mpsc::IntoIter]. -/// -/// Returns 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`. -/// -/// # 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, BlissResult}; -/// -/// fn main() -> BlissResult<()> { -/// let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")]; -/// for (path, result) in analyze_paths(&paths) { -/// 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(()) -/// } -/// ``` -#[cfg(feature = "ffmpeg")] -pub fn analyze_paths, F: IntoIterator>( - paths: F, -) -> mpsc::IntoIter<(PathBuf, BlissResult)> { - let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()); - 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`. -/// -/// # 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, BlissResult}; -/// -/// fn main() -> BlissResult<()> { -/// let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")]; -/// for (path, result) in analyze_paths(&paths) { -/// 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(()) -/// } -/// ``` -#[cfg(feature = "ffmpeg")] -pub fn analyze_paths_with_cores, F: IntoIterator>( - paths: F, - number_cores: NonZeroUsize, -) -> mpsc::IntoIter<(PathBuf, BlissResult)> { - let mut cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()); - if cores > number_cores { - cores = number_cores; - } - let paths: Vec = paths.into_iter().map(|p| p.into()).collect(); - #[allow(clippy::type_complexity)] - let (tx, rx): ( - mpsc::Sender<(PathBuf, BlissResult)>, - mpsc::Receiver<(PathBuf, BlissResult)>, - ) = mpsc::channel(); - if paths.is_empty() { - return rx.into_iter(); - } - let mut handles = Vec::new(); - let mut chunk_length = paths.len() / cores; - if chunk_length == 0 { - chunk_length = paths.len(); - } - for chunk in paths.chunks(chunk_length) { - let tx_thread = tx.clone(); - let owned_chunk = chunk.to_owned(); - let child = thread::spawn(move || { - for path in owned_chunk { - info!("Analyzing file '{:?}'", path); - if let Some(extension) = Path::new(&path).extension() { - let extension = extension.to_string_lossy().to_lowercase(); - if extension == "cue" { - match BlissCue::songs_from_path(&path) { - Ok(songs) => { - for song in songs { - tx_thread.send((path.to_owned(), song)).unwrap(); - } - } - Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(), - }; - continue; - } - } - let song = Song::from_path(&path); - tx_thread.send((path.to_owned(), song)).unwrap(); - } - }); - handles.push(child); - } - - rx.into_iter() -} - #[cfg(test)] mod tests { use super::*; - #[cfg(test)] - #[cfg(feature = "ffmpeg")] - use pretty_assertions::assert_eq; #[test] fn test_send_song() { @@ -285,68 +133,4 @@ mod tests { fn assert_sync() {} assert_sync::(); } - - #[test] - #[cfg(feature = "ffmpeg")] - fn test_analyze_paths() { - let paths = vec![ - "./data/s16_mono_22_5kHz.flac", - "./data/testcue.cue", - "./data/white_noise.mp3", - "definitely-not-existing.foo", - "not-existing.foo", - ]; - let mut results = analyze_paths(&paths) - .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::>(); - results.sort(); - let expected_results = vec![ - ( - false, - PathBuf::from("./data/testcue.cue"), - Some(String::from( - "error happened while decoding file – while \ - opening format for file './data/not-existing.wav': \ - ffmpeg::Error(2: No such file or directory).", - )), - ), - ( - false, - PathBuf::from("definitely-not-existing.foo"), - Some(String::from( - "error happened while decoding file – while \ - opening format for file 'definitely-not-existing\ - .foo': ffmpeg::Error(2: No such file or directory).", - )), - ), - ( - false, - PathBuf::from("not-existing.foo"), - Some(String::from( - "error happened while decoding file – \ - while opening format for file 'not-existing.foo': \ - ffmpeg::Error(2: No such file or directory).", - )), - ), - (true, PathBuf::from("./data/s16_mono_22_5kHz.flac"), None), - (true, PathBuf::from("./data/testcue.cue/CUE_TRACK001"), None), - (true, PathBuf::from("./data/testcue.cue/CUE_TRACK002"), None), - (true, PathBuf::from("./data/testcue.cue/CUE_TRACK003"), None), - (true, PathBuf::from("./data/white_noise.mp3"), None), - ]; - - assert_eq!(results, expected_results); - - let mut results = analyze_paths_with_cores(&paths, NonZeroUsize::new(1).unwrap()) - .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::>(); - results.sort(); - assert_eq!(results, expected_results); - } } diff --git a/src/library.rs b/src/library.rs index bd644e3..895c330 100644 --- a/src/library.rs +++ b/src/library.rs @@ -71,12 +71,13 @@ //! ```no_run //! use anyhow::{Error, Result}; //! use bliss_audio::library::{BaseConfig, Library}; +//! use bliss_audio::decoder::bliss_ffmpeg::FFmpeg; //! use std::path::PathBuf; //! //! let config_path = Some(PathBuf::from("path/to/config/config.json")); //! let database_path = Some(PathBuf::from("path/to/config/bliss.db")); //! let config = BaseConfig::new(config_path, database_path, None)?; -//! let library: Library = Library::new(config)?; +//! let library: Library = Library::new(config)?; //! # Ok::<(), Error>(()) //! ``` //! Once this is done, you can simply load the library by doing @@ -108,7 +109,6 @@ //! "real-life" example, the //! [blissify](https://github.com/Polochon-street/blissify-rs)'s code is using //! [Library] to implement bliss for a MPD player. -use crate::analyze_paths_with_cores; use crate::cue::CueInfo; use crate::playlist::closest_album_to_group; use crate::playlist::closest_to_songs; @@ -132,12 +132,16 @@ use std::collections::{HashMap, HashSet}; use std::env; use std::fs; use std::fs::create_dir_all; +use std::marker::PhantomData; use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::Mutex; use std::thread; +#[cfg(feature = "ffmpeg")] +use crate::decoder::bliss_ffmpeg::FFmpeg as Decoder; +use crate::decoder::Decoder as DecoderTrait; use crate::Song; use crate::FEATURES_VERSION; use crate::{Analysis, BlissError, NUMBER_FEATURES}; @@ -305,12 +309,13 @@ impl AppConfigTrait for BaseConfig { /// Provide it either the `BaseConfig`, or a `Config` extending /// `BaseConfig`. /// TODO code example -pub struct Library { +pub struct Library { /// The configuration struct, containing both information /// from `BaseConfig` as well as user-defined values. pub config: Config, /// SQL connection to the database. pub sqlite_conn: Arc>, + decoder: PhantomData, } /// Struct holding both a Bliss song, as well as any extra info @@ -355,7 +360,7 @@ impl AsRef for LibrarySong { // TODO should it really use anyhow errors? // TODO make sure that the path to string is consistent // TODO make a function that returns a list of all analyzed songs in the db -impl Library { +impl Library { /// Create a new [Library] object from the given Config struct that /// implements the [AppConfigTrait]. /// writing the configuration to the file given in @@ -419,9 +424,10 @@ impl Library { [], )?; config.write()?; - Ok(Library { + Ok(Self { config, sqlite_conn: Arc::new(Mutex::new(sqlite_conn)), + decoder: PhantomData, }) } @@ -436,9 +442,10 @@ impl Library { let data = fs::read_to_string(config_path)?; let config = Config::deserialize_config(&data)?; let sqlite_conn = Connection::open(&config.base_config().database_path)?; - let mut library = Library { + let mut library = Self { config, sqlite_conn: Arc::new(Mutex::new(sqlite_conn)), + decoder: PhantomData, }; if !library.version_sanity_check()? { warn!( @@ -811,7 +818,7 @@ impl Library { .collect(); let mut cue_extra_info: HashMap = HashMap::new(); - let results = analyze_paths_with_cores( + let results = Decoder::analyze_paths_with_cores( paths_extra_info.keys(), self.config.base_config().number_cores, ); @@ -1321,6 +1328,10 @@ mod test { use std::{convert::TryInto, fmt::Debug, sync::MutexGuard, time::Duration}; use tempdir::TempDir; + #[cfg(feature = "ffmpeg")] + use crate::song::decoder::bliss_ffmpeg::FFmpeg as Decoder; + use crate::song::decoder::Decoder as DecoderTrait; + #[derive(Deserialize, Serialize, Debug, PartialEq, Clone, Default)] struct ExtraInfo { ignore: bool, @@ -1355,7 +1366,7 @@ mod test { // Setup a test library made of 3 analyzed songs, with every field being different, // as well as an unanalyzed song and a song analyzed with a previous version. fn setup_test_library() -> ( - Library, + Library, TempDir, ( LibrarySong, @@ -1370,9 +1381,12 @@ mod test { let config_dir = TempDir::new("coucou").unwrap(); let config_file = config_dir.path().join("config.json"); let database_file = config_dir.path().join("bliss.db"); - let library = - Library::::new_from_base(Some(config_file), Some(database_file), None) - .unwrap(); + let library = Library::::new_from_base( + Some(config_file), + Some(database_file), + None, + ) + .unwrap(); let analysis_vector: [f32; NUMBER_FEATURES] = (0..NUMBER_FEATURES) .map(|x| x as f32 / 10.) @@ -2344,7 +2358,7 @@ mod test { .iter() .zip(vec![(), ()].into_iter()) .map(|(path, expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -2399,7 +2413,7 @@ mod test { .into_iter(), ) .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -2463,7 +2477,7 @@ mod test { .into_iter(), ) .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -2498,7 +2512,7 @@ mod test { }; let expected_song = { LibrarySong { - bliss_song: Song::from_path("./data/s16_mono_22_5kHz.flac").unwrap(), + bliss_song: Decoder::song_from_path("./data/s16_mono_22_5kHz.flac").unwrap(), extra_info: ExtraInfo { ignore: true, metadata_bliss_does_not_have: String::from("coucou"), @@ -2579,7 +2593,7 @@ mod test { .try_into() .unwrap(), }; - let expected_analysis_vector = Song::from_path(path).unwrap().analysis; + let expected_analysis_vector = Decoder::song_from_path(path).unwrap().analysis; assert_eq!(analysis_vector, expected_analysis_vector); } @@ -2618,7 +2632,7 @@ mod test { .iter() .zip(vec![(), ()].into_iter()) .map(|(path, expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -2666,7 +2680,7 @@ mod test { .iter() .zip(vec![true, false].into_iter()) .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -2733,7 +2747,7 @@ mod test { .into_iter(), ) .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -2811,7 +2825,7 @@ mod test { .into_iter(), ) .map(|((path, _extra_info), expected_extra_info)| LibrarySong { - bliss_song: Song::from_path(path).unwrap(), + bliss_song: Decoder::song_from_path(path).unwrap(), extra_info: expected_extra_info, }) .collect::>>(); @@ -3068,7 +3082,8 @@ mod test { #[test] fn test_from_config_path_non_existing() { assert!( - Library::::from_config_path(Some(PathBuf::from("non-existing"))).is_err() + Library::::from_config_path(Some(PathBuf::from("non-existing"))) + .is_err() ); } @@ -3097,11 +3112,12 @@ mod test { // get the stored song. let song = _generate_library_song(None); { - let mut library = Library::new(config.to_owned()).unwrap(); + let mut library = Library::<_, Decoder>::new(config.to_owned()).unwrap(); library.store_song(&song).unwrap(); } - let library: Library = Library::from_config_path(Some(config_file)).unwrap(); + let library: Library = + Library::from_config_path(Some(config_file)).unwrap(); let connection = library.sqlite_conn.lock().unwrap(); let returned_song = _library_song_from_database(connection, &song.bliss_song.path.to_string_lossy()); @@ -3203,8 +3219,12 @@ mod test { assert!(!config_dir.is_dir()); let config_file = config_dir.join("config.json"); let database_file = config_dir.join("bliss.db"); - Library::::new_from_base(Some(config_file), Some(database_file), Some(nzus(1))) - .unwrap(); + Library::::new_from_base( + Some(config_file), + Some(database_file), + Some(nzus(1)), + ) + .unwrap(); assert!(config_dir.is_dir()); } } diff --git a/src/misc.rs b/src/misc.rs index d448016..80d0635 100644 --- a/src/misc.rs +++ b/src/misc.rs @@ -64,14 +64,16 @@ impl Normalize for LoudnessDesc { mod tests { use super::*; #[cfg(feature = "ffmpeg")] - use crate::Song; + use crate::song::decoder::bliss_ffmpeg::FFmpeg as Decoder; + #[cfg(feature = "ffmpeg")] + use crate::song::decoder::Decoder as DecoderTrait; #[cfg(feature = "ffmpeg")] use std::path::Path; #[test] #[cfg(feature = "ffmpeg")] fn test_loudness() { - let song = Song::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); + let song = Decoder::decode(Path::new("data/s16_mono_22_5kHz.flac")).unwrap(); let mut loudness_desc = LoudnessDesc::default(); for chunk in song.sample_array.chunks_exact(LoudnessDesc::WINDOW_SIZE) { loudness_desc.do_(&chunk); diff --git a/src/song.rs b/src/song.rs index b1efb5a..aa924cb 100644 --- a/src/song.rs +++ b/src/song.rs @@ -17,39 +17,12 @@ use crate::misc::LoudnessDesc; use crate::temporal::BPMDesc; use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc}; use crate::{BlissError, BlissResult, SAMPLE_RATE}; -#[cfg(feature = "ffmpeg")] -use crate::{CHANNELS, FEATURES_VERSION}; -#[cfg(feature = "ffmpeg")] -use ::log::warn; use core::ops::Index; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::codec::threading::{Config, Type as ThreadingType}; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::channel_layout::ChannelLayout; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::error::Error; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::error::EINVAL; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::format::sample::{Sample, Type}; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::frame::audio::Audio; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::log; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::util::log::level::Level; -#[cfg(feature = "ffmpeg")] -use ffmpeg_next::{media, util}; use ndarray::{arr1, Array1}; use std::convert::TryInto; use std::fmt; -#[cfg(feature = "ffmpeg")] -use std::path::Path; + use std::path::PathBuf; -#[cfg(feature = "ffmpeg")] -use std::sync::mpsc; -#[cfg(feature = "ffmpeg")] -use std::sync::mpsc::Receiver; use std::thread; use std::time::Duration; use strum::{EnumCount, IntoEnumIterator}; @@ -73,7 +46,7 @@ pub struct Song { /// Song's tracked number, read from the metadata /// TODO normalize this into an integer pub track_number: Option, - /// Song's genre, read from the metadata (`""` if empty) + /// Song's genre, read from the metadata pub genre: Option, /// bliss analysis results pub analysis: Analysis, @@ -108,7 +81,7 @@ impl AsRef for Song { /// use bliss_audio::{AnalysisIndex, BlissResult, Song}; /// /// fn main() -> BlissResult<()> { -/// let song = Song::from_path("path/to/song")?; +/// let song = Decoder::song_from_path("path/to/song")?; /// println!("{}", song.analysis[AnalysisIndex::Tempo]); /// Ok(()) /// } @@ -212,47 +185,628 @@ impl Analysis { } } -impl Song { - /// Returns a decoded [Song] given a file path, or an error if the song - /// could not be analyzed for some reason. - /// - /// # Arguments +/// TODO move me to its own module +pub mod decoder { + use log::info; + + use crate::{cue::BlissCue, BlissError, BlissResult, Song, FEATURES_VERSION}; + use std::{ + num::NonZeroUsize, + path::{Path, PathBuf}, + sync::mpsc, + thread, + time::Duration, + }; + + #[derive(Default, Debug)] + /// A struct used to represent a song that has been decoded, but not analyzed yet. /// - /// * `path` - A [Path] holding a valid file path to a valid audio file. + /// Most users will not need to use it, as most users won't implement + /// their decoders, but rely on `ffmpeg` to decode songs, and use `FFmpeg::song_from_path`. /// - /// # Errors + /// Since it contains the fully decoded song inside of + /// `PreAnalyzedSong::sample_array`, it will be very large. Users should + /// convert it to a `Song` as soon as possible, since it is this + /// structure's only reason to be. + pub struct PreAnalyzedSong { + /// Song's provided file path + pub path: PathBuf, + /// Song's artist, read from the metadata + pub artist: Option, + /// Song's album's artist name, read from the metadata + pub album_artist: Option, + /// Song's title, read from the metadata + pub title: Option, + /// Song's album name, read from the metadata + pub album: Option, + /// Song's tracked number, read from the metadata + /// TODO normalize this into an integer + pub track_number: Option, + /// Song's genre, read from the metadata + pub genre: Option, + /// The song's duration + pub duration: Duration, + /// An array of the song's decoded sample which should be, + /// prior to analysis, resampled to f32le, one channel, with a sampling rate + /// of 22050 Hz. Anything other than that will yield wrong results. + /// To double-check that your sample array has the right format, you could run + /// `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le -f hash -hash addler32 -`, + /// which will give you the addler32 checksum of the sample array if the song + /// has been decoded properly. You can then compute the addler32 checksum of your sample + /// array (see `_test_decode` in the tests) and make sure both are the same. + /// + /// (Running `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le` will simply give + /// you the raw sample array as it should look like, if you're not into computing checksums) + pub sample_array: Vec, + } + + impl TryFrom for Song { + type Error = BlissError; + + fn try_from(raw_song: PreAnalyzedSong) -> BlissResult { + Ok(Song { + path: raw_song.path, + artist: raw_song.artist, + album_artist: raw_song.album_artist, + title: raw_song.title, + album: raw_song.album, + track_number: raw_song.track_number, + genre: raw_song.genre, + duration: raw_song.duration, + analysis: Song::analyze(&raw_song.sample_array)?, + features_version: FEATURES_VERSION, + cue_info: None, + }) + } + } + + /// Trait used to implement your own decoder. /// - /// This function will return an error if the file path is invalid, if - /// the file path points to a file containing no or corrupted audio stream, - /// or if the analysis could not be conducted to the end for some reason. + /// Once the `decode` method is implemented, which should decode and + /// resample a song to one channel, 22050 Hz f32le, several functions + /// to perform analysis from path(s) are available, such a + /// [song_from_path] and [analyze_paths]. /// - /// The error type returned should give a hint as to whether it was a - /// decoding ([DecodingError](BlissError::DecodingError)) or an analysis - /// ([AnalysisError](BlissError::AnalysisError)) error. + /// For a reference on how to implement that trait, look at the [FFmpeg] decoder + pub trait Decoder { + /// A function that should decode and resample a song, optionally + /// extracting the song's metadata such as the artist, the album, etc. + /// + /// The output sample array should be resampled to f32le, one channel, with a sampling rate + /// of 22050 Hz. Anything other than that will yield wrong results. + /// To double-check that your sample array has the right format, you could run + /// `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le -f hash -hash addler32 -`, + /// which will give you the addler32 checksum of the sample array if the song + /// has been decoded properly. You can then compute the addler32 checksum of your sample + /// array (see `_test_decode` in the tests) and make sure both are the same. + /// + /// (Running `ffmpeg -i path_to_your_song.flac -ar 22050 -ac 1 -c:a pcm_f32le` will simply give + /// you the raw sample array as it should look like, if you're not into computing checksums) + fn decode(path: &Path) -> BlissResult; + + /// Returns a decoded [Song] given a file path, or an error if the song + /// could not be analyzed for some reason. + /// + /// # Arguments + /// + /// * `path` - A [Path] holding a valid file path to a valid audio file. + /// + /// # Errors + /// + /// This function will return an error if the file path is invalid, if + /// the file path points to a file containing no or corrupted audio stream, + /// or if the analysis could not be conducted to the end for some reason. + /// + /// The error type returned should give a hint as to whether it was a + /// decoding ([DecodingError](BlissError::DecodingError)) or an analysis + /// ([AnalysisError](BlissError::AnalysisError)) error. + fn song_from_path>(path: P) -> BlissResult { + Self::decode(path.as_ref())?.try_into() + } + + /// Analyze songs in `paths`, and return the analyzed [Song] objects through an + /// [mpsc::IntoIter]. + /// + /// Returns 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`. + /// + /// # 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). + /// + /// This example uses FFmpeg to decode songs by default, but it is possible to + /// implement another decoder and replace `use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder;` + /// by a custom decoder. + /// + #[cfg_attr( + feature = "ffmpeg", + doc = r##" +# Example + +```no_run +use bliss_audio::{BlissResult}; +use bliss_audio::decoder::Decoder as DecoderTrait; +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; + +fn main() -> BlissResult<()> { + let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")]; + for (path, result) in Decoder::analyze_paths(&paths) { + 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(()) +} +```"## + )] + fn analyze_paths, F: IntoIterator>( + paths: F, + ) -> mpsc::IntoIter<(PathBuf, BlissResult)> { + let cores = thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()); + Self::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`. + /// + /// # 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). + #[cfg_attr( + feature = "ffmpeg", + doc = r##" +# Example + +```no_run +use bliss_audio::BlissResult; +use bliss_audio::decoder::Decoder as DecoderTrait; +use bliss_audio::decoder::bliss_ffmpeg::FFmpeg as Decoder; + +fn main() -> BlissResult<()> { + let paths = vec![String::from("/path/to/song1"), String::from("/path/to/song2")]; + for (path, result) in Decoder::analyze_paths(&paths) { + 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(()) +} +```"## + )] + fn analyze_paths_with_cores, F: IntoIterator>( + paths: F, + number_cores: NonZeroUsize, + ) -> mpsc::IntoIter<(PathBuf, BlissResult)> { + let mut cores = + thread::available_parallelism().unwrap_or(NonZeroUsize::new(1).unwrap()); + if cores > number_cores { + cores = number_cores; + } + let paths: Vec = paths.into_iter().map(|p| p.into()).collect(); + #[allow(clippy::type_complexity)] + let (tx, rx): ( + mpsc::Sender<(PathBuf, BlissResult)>, + mpsc::Receiver<(PathBuf, BlissResult)>, + ) = mpsc::channel(); + if paths.is_empty() { + return rx.into_iter(); + } + let mut handles = Vec::new(); + let mut chunk_length = paths.len() / cores; + if chunk_length == 0 { + chunk_length = paths.len(); + } + for chunk in paths.chunks(chunk_length) { + let tx_thread = tx.clone(); + let owned_chunk = chunk.to_owned(); + let child = thread::spawn(move || { + for path in owned_chunk { + info!("Analyzing file '{:?}'", path); + if let Some(extension) = Path::new(&path).extension() { + let extension = extension.to_string_lossy().to_lowercase(); + if extension == "cue" { + match BlissCue::::songs_from_path(&path) { + Ok(songs) => { + for song in songs { + tx_thread.send((path.to_owned(), song)).unwrap(); + } + } + Err(e) => tx_thread.send((path.to_owned(), Err(e))).unwrap(), + }; + continue; + } + } + let song = Self::song_from_path(&path); + tx_thread.send((path.to_owned(), song)).unwrap(); + } + }); + handles.push(child); + } + + rx.into_iter() + } + } + #[cfg(feature = "ffmpeg")] - pub fn from_path>(path: P) -> BlissResult { - let raw_song = Song::decode(path.as_ref())?; - - Ok(Song { - path: raw_song.path, - artist: raw_song.artist, - album_artist: raw_song.album_artist, - title: raw_song.title, - album: raw_song.album, - track_number: raw_song.track_number, - genre: raw_song.genre, - duration: raw_song.duration, - analysis: Song::analyze(&raw_song.sample_array)?, - features_version: FEATURES_VERSION, - cue_info: None, - }) + /// The default decoder module. It uses [ffmpeg](https://ffmpeg.org/) in + /// order to decode and resample songs. A very good choice for 99% of + /// the users. + pub mod bliss_ffmpeg { + use super::{Decoder, PreAnalyzedSong}; + use crate::{BlissError, BlissResult, CHANNELS, SAMPLE_RATE}; + use ::log::warn; + use ffmpeg_next as ffmpeg; + use ffmpeg_next::codec::threading::{Config, Type as ThreadingType}; + use ffmpeg_next::util::channel_layout::ChannelLayout; + use ffmpeg_next::util::error::Error; + use ffmpeg_next::util::error::EINVAL; + use ffmpeg_next::util::format::sample::{Sample, Type}; + use ffmpeg_next::util::frame::audio::Audio; + use ffmpeg_next::util::log; + use ffmpeg_next::util::log::level::Level; + use ffmpeg_next::{media, util}; + use std::sync::mpsc; + use std::sync::mpsc::Receiver; + use std::thread; + use std::time::Duration; + + use std::path::Path; + + /// The actual FFmpeg decoder. + /// + /// To use it, one might `use FFmpeg as Decoder;`, + /// `use super::decoder::Decoder as DecoderTrait;`, and then use + /// `Decoder::song_from_path` + pub struct FFmpeg; + + impl FFmpeg { + fn resample_frame( + rx: Receiver