diff --git a/Cargo.lock b/Cargo.lock index 86e8da4..8b7c33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "noisy_float", "num_cpus", "rayon", + "rcue", "ripemd160", "rustfft", "serde", @@ -993,6 +994,12 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "rcue" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe64124d8a42e46e01d60b00707c9c741fe7efb65267cac6c8e5bfaf390e7af5" + [[package]] name = "regex" version = "0.1.80" diff --git a/Cargo.toml b/Cargo.toml index b2e9891..708fbd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ bliss-audio-aubio-rs = "0.2.0" strum = "0.21" strum_macros = "0.21" serde = { version = "1.0", optional = true, features = ["derive"] } +rcue = "0.1.1" [dev-dependencies] mime_guess = "2.0.3" diff --git a/data/testcue.cue b/data/testcue.cue new file mode 100644 index 0000000..fb6bd06 --- /dev/null +++ b/data/testcue.cue @@ -0,0 +1,17 @@ +REM GENRE Random +REM DATE 2022 +PERFORMER "Polochon_street" +TITLE "Album for CUE test" +FILE "testcue.wav" WAVE + TRACK 01 AUDIO + TITLE "Renaissance" + PERFORMER "David TMX" + INDEX 01 0:00:00 + TRACK 02 AUDIO + TITLE "Piano" + PERFORMER "Polochon_street" + INDEX 01 0:11:05 + TRACK 03 AUDIO + TITLE "Tone" + PERFORMER "Polochon_street" + INDEX 01 0:16:69 diff --git a/data/testcue.wav b/data/testcue.wav new file mode 100644 index 0000000..cbd9832 Binary files /dev/null and b/data/testcue.wav differ diff --git a/src/lib.rs b/src/lib.rs index cf0951d..f05c620 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ use std::sync::mpsc; use std::thread; use thiserror::Error; -pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES}; +pub use song::{Analysis, AnalysisIndex, BlissCue, BlissCueFile, Song, NUMBER_FEATURES}; const CHANNELS: u16 = 1; const SAMPLE_RATE: u32 = 22050; diff --git a/src/song.rs b/src/song.rs index 6f5d7e5..5b7c9a0 100644 --- a/src/song.rs +++ b/src/song.rs @@ -34,6 +34,8 @@ use ffmpeg_next::util::frame::audio::Audio; use ffmpeg_next::util::log; use ffmpeg_next::util::log::level::Level; use ndarray::{arr1, Array1}; +use rcue::cue::{Cue, Track}; +use rcue::parser::parse_from_file; use std::convert::TryInto; use std::fmt; use std::path::Path; @@ -202,6 +204,133 @@ impl Analysis { } } +#[allow(missing_docs)] +pub struct BlissCue { + cue: Cue, + path: String, +} + +#[allow(missing_docs)] +pub struct BlissCueFile { + sample_array: Vec, + album: Option, + artist: Option, + genre: Option, + tracks: Vec, + path: String, +} + +impl BlissCue { + #[allow(missing_docs)] + pub fn from_path + std::fmt::Debug>(path: P) -> BlissResult { + let cue = parse_from_file(&path.as_ref().to_string_lossy(), false).map_err(|e| { + BlissError::DecodingError(format!("when opening CUE file '{:?}': {:?}", path, e)) + })?; + Ok(BlissCue { + cue, + path: path.as_ref().to_string_lossy().to_string(), + }) + } + + #[allow(missing_docs)] + pub fn files(&self) -> BlissResult> { + let mut cue_files = Vec::new(); + for cue_file in self.cue.files.iter() { + let cue_path = match Path::new(&self.path).parent() { + Some(parent) => format!("{}/{}", parent.to_string_lossy(), cue_file.file), + None => cue_file.file.to_string(), + }; + let genre = self + .cue + .comments + .iter() + .find(|(c, _)| c == "GENRE") + .map(|(_, v)| v.to_owned()); + let sample_array = Song::decode(Path::new(&cue_path))?.sample_array; + let bliss_cue_file = BlissCueFile { + sample_array, + genre, + artist: self.cue.performer.to_owned(), + album: self.cue.title.to_owned(), + tracks: cue_file.tracks.to_owned(), + path: cue_path, + }; + cue_files.push(bliss_cue_file) + } + Ok(cue_files) + } +} + +impl BlissCueFile { + #[allow(missing_docs)] + pub fn get_songs(&self) -> Vec> { + let mut songs = Vec::new(); + for (index, tuple) in (&self.tracks[..]).windows(2).enumerate() { + 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((_, end_current)) = next_track.indices.get(0) { + // TODO remove unwraps here + let start_current = (start_current.as_secs_f32() * SAMPLE_RATE as f32) as usize; + let end_current = (end_current.as_secs_f32() * SAMPLE_RATE as f32) as usize; + // TODO to_vec, really? + let analysis = + Song::analyze(self.sample_array[start_current..end_current].to_vec()); + if let Ok(a) = analysis { + let song = Song { + path: PathBuf::from(format!("{}/CUE_TRACK{:03}", self.path, index + 1)), + album: self.album.to_owned(), + artist: current_track.performer.to_owned(), + album_artist: self.artist.to_owned(), + analysis: a, + duration: Duration::from_secs_f32( + (end_current - start_current) as f32 / SAMPLE_RATE as f32, + ), + genre: self.genre.to_owned(), + title: current_track.title, + track_number: Some(current_track.no), + features_version: FEATURES_VERSION, + }; + songs.push(Ok(song)); + } else { + songs.push(Err(analysis.unwrap_err())) + }; + } + } + } + // TODO put into a function + if let Some(last_track) = self.tracks.last() { + if let Some((_, start_current)) = last_track.indices.get(0) { + let start_current = (start_current.as_secs_f32() * SAMPLE_RATE as f32) as usize; + let analysis = Song::analyze(self.sample_array[start_current..].to_vec()); + if let Ok(a) = analysis { + let song = Song { + path: PathBuf::from(format!( + "{}/CUE_TRACK{:03}", + self.path, + self.tracks.len() + )), + album: self.album.to_owned(), + artist: self.artist.to_owned(), + album_artist: self.artist.to_owned(), + analysis: a, + duration: Duration::from_secs_f32( + (self.sample_array.len() - start_current) as f32 / SAMPLE_RATE as f32, + ), + genre: self.genre.to_owned(), + title: last_track.title.to_owned(), + track_number: Some(last_track.no.to_owned()), + features_version: FEATURES_VERSION, + }; + songs.push(Ok(song)); + } else { + songs.push(Err(analysis.unwrap_err())) + }; + } + } + songs + } +} + impl Song { #[allow(dead_code)] /// Compute the distance between the current song and any given @@ -1009,6 +1138,125 @@ mod tests { ], ); } + + #[test] + fn test_cue_analysis() { + let cue = BlissCue::from_path(Path::new("data/testcue.cue")).unwrap(); + let cue_files = cue.files().unwrap(); + let cue_file = cue_files.first().unwrap(); + let songs = cue_file.get_songs(); + let expected = vec![ + Ok(Song { + path: Path::new("data/testcue.wav/CUE_TRACK001").to_path_buf(), + analysis: Analysis { + internal_analysis: [ + 0.38463724, + -0.85219246, + -0.761946, + -0.8904667, + -0.63892543, + -0.73945934, + -0.8004017, + -0.8237293, + 0.33865356, + 0.32481194, + -0.35692245, + -0.6355889, + -0.29584837, + 0.06431806, + 0.21875131, + -0.58104205, + -0.9466792, + -0.94811195, + -0.9820919, + -0.9596871, + ], + }, + album: Some(String::from("Album for CUE test")), + artist: Some(String::from("David TMX")), + title: Some(String::from("Renaissance")), + genre: Some(String::from("Random")), + track_number: Some(String::from("01")), + features_version: FEATURES_VERSION, + album_artist: Some(String::from("Polochon_street")), + duration: Duration::from_secs_f32(11.066666603), + ..Default::default() + }), + Ok(Song { + path: Path::new("data/testcue.wav/CUE_TRACK002").to_path_buf(), + analysis: Analysis { + internal_analysis: [ + 0.18622077, + -0.5989029, + -0.5554645, + -0.6343865, + -0.24163479, + -0.25766593, + -0.40616858, + -0.23334873, + 0.76875293, + 0.7785741, + -0.5075115, + -0.5272629, + -0.56706166, + -0.568486, + -0.5639081, + -0.5706943, + -0.96501005, + -0.96501285, + -0.9649896, + -0.96498996, + ], + }, + features_version: FEATURES_VERSION, + album: Some(String::from("Album for CUE test")), + artist: Some(String::from("Polochon_street")), + title: Some(String::from("Piano")), + genre: Some(String::from("Random")), + track_number: Some(String::from("02")), + album_artist: Some(String::from("Polochon_street")), + duration: Duration::from_secs_f64(5.853333473), + ..Default::default() + }), + Ok(Song { + path: Path::new("data/testcue.wav/CUE_TRACK003").to_path_buf(), + analysis: Analysis { + internal_analysis: [ + 0.0024261475, + 0.9874661, + 0.97330654, + -0.9724426, + 0.99678576, + -0.9961549, + -0.9840142, + -0.9269961, + 0.7498772, + 0.22429907, + -0.8355152, + -0.9977258, + -0.9977849, + -0.997785, + -0.99778515, + -0.997785, + -0.99999976, + -0.99999976, + -0.99999976, + -0.99999976, + ], + }, + album: Some(String::from("Album for CUE test")), + artist: Some(String::from("Polochon_street")), + title: Some(String::from("Tone")), + genre: Some(String::from("Random")), + track_number: Some(String::from("03")), + features_version: FEATURES_VERSION, + album_artist: Some(String::from("Polochon_street")), + duration: Duration::from_secs_f32(5.586666584), + ..Default::default() + }), + ]; + assert_eq!(expected, songs); + } } #[cfg(all(feature = "bench", test))]