Add a BlissCue
/ BlissCueFile
struct
This commit is contained in:
parent
5302e1e45c
commit
0227c9596a
6 changed files with 274 additions and 1 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -103,6 +103,7 @@ dependencies = [
|
||||||
"noisy_float",
|
"noisy_float",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"rcue",
|
||||||
"ripemd160",
|
"ripemd160",
|
||||||
"rustfft",
|
"rustfft",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -993,6 +994,12 @@ dependencies = [
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rcue"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe64124d8a42e46e01d60b00707c9c741fe7efb65267cac6c8e5bfaf390e7af5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "0.1.80"
|
version = "0.1.80"
|
||||||
|
|
|
@ -45,6 +45,7 @@ bliss-audio-aubio-rs = "0.2.0"
|
||||||
strum = "0.21"
|
strum = "0.21"
|
||||||
strum_macros = "0.21"
|
strum_macros = "0.21"
|
||||||
serde = { version = "1.0", optional = true, features = ["derive"] }
|
serde = { version = "1.0", optional = true, features = ["derive"] }
|
||||||
|
rcue = "0.1.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mime_guess = "2.0.3"
|
mime_guess = "2.0.3"
|
||||||
|
|
17
data/testcue.cue
Normal file
17
data/testcue.cue
Normal file
|
@ -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
|
BIN
data/testcue.wav
Normal file
BIN
data/testcue.wav
Normal file
Binary file not shown.
|
@ -77,7 +77,7 @@ use std::sync::mpsc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use thiserror::Error;
|
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 CHANNELS: u16 = 1;
|
||||||
const SAMPLE_RATE: u32 = 22050;
|
const SAMPLE_RATE: u32 = 22050;
|
||||||
|
|
248
src/song.rs
248
src/song.rs
|
@ -34,6 +34,8 @@ use ffmpeg_next::util::frame::audio::Audio;
|
||||||
use ffmpeg_next::util::log;
|
use ffmpeg_next::util::log;
|
||||||
use ffmpeg_next::util::log::level::Level;
|
use ffmpeg_next::util::log::level::Level;
|
||||||
use ndarray::{arr1, Array1};
|
use ndarray::{arr1, Array1};
|
||||||
|
use rcue::cue::{Cue, Track};
|
||||||
|
use rcue::parser::parse_from_file;
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::path::Path;
|
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<f32>,
|
||||||
|
album: Option<String>,
|
||||||
|
artist: Option<String>,
|
||||||
|
genre: Option<String>,
|
||||||
|
tracks: Vec<Track>,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlissCue {
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub fn from_path<P: AsRef<Path> + std::fmt::Debug>(path: P) -> BlissResult<Self> {
|
||||||
|
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<Vec<BlissCueFile>> {
|
||||||
|
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<BlissResult<Song>> {
|
||||||
|
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 {
|
impl Song {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
/// Compute the distance between the current song and any given
|
/// 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))]
|
#[cfg(all(feature = "bench", test))]
|
||||||
|
|
Loading…
Reference in a new issue