From b0a96e257ec99029d781d558e7cb3288a547a357 Mon Sep 17 00:00:00 2001 From: Polochon-street Date: Tue, 1 Jun 2021 18:02:15 +0200 Subject: [PATCH] Change `analysis` from Vec to `Analysis` --- CHANGELOG.md | 8 +- Cargo.lock | 270 +++++++++++++++++++++++++++++++++++++++++--- Cargo.toml | 6 +- examples/analyse.rs | 2 +- src/lib.rs | 49 +++++++- src/library.rs | 48 ++++---- src/song.rs | 238 ++++++++++++++++++++++++++++---------- 7 files changed, 514 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a2131..7357a83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ -Change Log +# Changelog -All user visible changes to this project will be documented in this file. This project adheres to Semantic Versioning, as described for Rust libraries in RFC #1105 +## bliss 0.2.1 + +* Added an `Analysis` struct to `Song`, as well as an `AnalysisIndex` to + index it easily. +* Changed some logging parameters for the Library trait. diff --git a/Cargo.lock b/Cargo.lock index d8d816a..cc0b313 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,15 +6,30 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" +dependencies = [ + "memchr 0.1.11", +] + [[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ - "memchr", + "memchr 2.4.0", ] +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + [[package]] name = "atty" version = "0.2.14" @@ -23,7 +38,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -42,12 +57,12 @@ dependencies = [ "cexpr", "cfg-if 0.1.10", "clang-sys", - "lazy_static", + "lazy_static 1.4.0", "lazycell", "peeking_take_while", "proc-macro2", "quote", - "regex", + "regex 1.5.4", "rustc-hash", "shlex", ] @@ -60,13 +75,13 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bliss-audio" -version = "0.1.3" +version = "0.2.3" dependencies = [ "bliss-audio-aubio-rs", "crossbeam", "env_logger", "ffmpeg-next", - "lazy_static", + "lazy_static 1.4.0", "log", "ndarray", "ndarray-npy", @@ -77,6 +92,8 @@ dependencies = [ "ripemd160", "rustfft", "serde", + "strum", + "strum_macros", "thiserror", ] @@ -96,6 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ef9fab7b922bdd057bb06fa2a2fa79d2a93bec3dd576320511cb3dfe21e78a9" dependencies = [ "cc", + "fftw-sys", ] [[package]] @@ -140,6 +158,27 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.10+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cc" version = "1.0.67" @@ -170,6 +209,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" +dependencies = [ + "num", + "time", +] + [[package]] name = "clang-sys" version = "0.29.3" @@ -233,7 +282,7 @@ checksum = "52fb27eab85b17fbb9f6fd667089e07d6a2eb8743d02639ee7f6a7a7729c9c94" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", - "lazy_static", + "lazy_static 1.4.0", "memoffset", "scopeguard", ] @@ -256,7 +305,7 @@ checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" dependencies = [ "autocfg", "cfg-if 1.0.0", - "lazy_static", + "lazy_static 1.4.0", ] [[package]] @@ -292,7 +341,7 @@ dependencies = [ "atty", "humantime", "log", - "regex", + "regex 1.5.4", "termcolor", ] @@ -327,6 +376,30 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "fftw-src" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08962470ab0e91e74ec7d338c8731476c28ed4e503a3080b0f001692e395a7c" +dependencies = [ + "anyhow", + "cc", + "fs_extra", + "ftp", + "zip", +] + +[[package]] +name = "fftw-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e3951d695cc2f17610cd041e87ebc15078d1af5eb8c6be77921381fc98b3fd" +dependencies = [ + "fftw-src", + "libc", + "num-complex 0.3.1", +] + [[package]] name = "flate2" version = "1.0.20" @@ -339,6 +412,23 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "ftp" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542951aad0071952c27409e3bd7cb62d1a3ad419c4e7314106bf994e0083ad5d" +dependencies = [ + "chrono", + "lazy_static 0.1.16", + "regex 0.1.80", +] + [[package]] name = "generic-array" version = "0.12.4" @@ -381,6 +471,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.18" @@ -424,6 +523,22 @@ dependencies = [ "libc", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazy_static" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" + [[package]] name = "lazy_static" version = "1.4.0" @@ -449,7 +564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" dependencies = [ "cc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -476,6 +591,15 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" +dependencies = [ + "libc", +] + [[package]] name = "memchr" version = "2.4.0" @@ -558,10 +682,21 @@ version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" dependencies = [ - "memchr", + "memchr 2.4.0", "version_check", ] +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-integer", + "num-iter", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.0" @@ -601,6 +736,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -800,21 +946,40 @@ dependencies = [ "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "lazy_static", + "lazy_static 1.4.0", "num_cpus", ] +[[package]] +name = "regex" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f" +dependencies = [ + "aho-corasick 0.5.3", + "memchr 0.1.11", + "regex-syntax 0.3.9", + "thread_local", + "utf8-ranges", +] + [[package]] name = "regex" version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "aho-corasick 0.7.18", + "memchr 2.4.0", + "regex-syntax 0.6.25", ] +[[package]] +name = "regex-syntax" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957" + [[package]] name = "regex-syntax" version = "0.6.25" @@ -902,6 +1067,24 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" +[[package]] +name = "strum" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" + +[[package]] +name = "strum_macros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb076d8b589fde927ec90d05920d610554ca3a4d9dddb177481cadd071a19c2e" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.72" @@ -942,6 +1125,35 @@ dependencies = [ "syn", ] +[[package]] +name = "thread-id" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" +dependencies = [ + "kernel32-sys", + "libc", +] + +[[package]] +name = "thread_local" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5" +dependencies = [ + "thread-id", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "transpose" version = "0.2.1" @@ -964,12 +1176,24 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicode-segmentation" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" + [[package]] name = "unicode-xid" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" + [[package]] name = "vcpkg" version = "0.2.12" @@ -988,6 +1212,12 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -998,6 +1228,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -1010,7 +1246,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1026,7 +1262,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c83dc9b784d252127720168abd71ea82bf8c3d96b17dc565b5e2a02854f2b27" dependencies = [ "byteorder", + "bzip2", "crc32fast", "flate2", "thiserror", + "time", ] diff --git a/Cargo.toml b/Cargo.toml index a15b385..62868de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "bliss-audio" -version = "0.1.3" +version = "0.2.0" authors = ["Polochon-street "] edition = "2018" license = "GPL-3.0-only" description = "A song analysis library for making playlists" homepage = "https://lelele.io/bliss.html" repository = "https://github.com/Polochon-street/bliss-rs" -keywords = ["audio", "analysis", "MIR", "playlist"] +keywords = ["audio", "analysis", "MIR", "playlist", "similarity"] readme = "README.md" [package.metadata.docs.rs] @@ -41,4 +41,6 @@ thiserror = "1.0.24" # Until https://github.com/aubio/aubio/issues/336 is somehow solved # Hopefully we'll be able to use the official aubio-rs at some point. bliss-audio-aubio-rs = "0.2.0" +strum = "0.21" +strum_macros = "0.21" serde = { version = "1.0", optional = true, features = ["derive"] } diff --git a/examples/analyse.rs b/examples/analyse.rs index 7d42dc8..0a9db25 100644 --- a/examples/analyse.rs +++ b/examples/analyse.rs @@ -10,7 +10,7 @@ fn main() { let args: Vec = env::args().skip(1).collect(); for path in &args { match Song::new(&path) { - Ok(song) => println!("{}: {:?}", path, song.analysis,), + Ok(song) => println!("{}: {:?}", path, song.analysis), Err(e) => println!("{}: {}", path, e), } } diff --git a/src/lib.rs b/src/lib.rs index b8d0246..567ffa0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +//! # bliss audio library +//! //! bliss is a library for making "smart" audio playlists. //! //! The core of the library is the `Song` object, which relates to a @@ -17,6 +19,49 @@ //! It should be as easy as implementing the necessary traits for [Library]. //! A reference implementation for the MPD player is available //! [here](https://github.com/Polochon-street/blissify-rs) +//! +//! # Examples +//! +//! ## Analyze & compute the distance between two songs +//! ```no_run +//! use bliss_audio::{BlissError, Song}; +//! +//! fn main() -> Result<(), BlissError> { +//! let song1 = Song::new("/path/to/song1")?; +//! let song2 = Song::new("/path/to/song2")?; +//! +//! println!("Distance between song1 and song2 is {}", song1.distance(&song2)); +//! Ok(()) +//! } +//! ``` +//! +//! ### Make a playlist from a song +//! ```no_run +//! use bliss_audio::{BlissError, Song}; +//! use ndarray::{arr1, Array}; +//! use noisy_float::prelude::n32; +//! +//! fn main() -> Result<(), BlissError> { +//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"]; +//! let mut songs: Vec = paths +//! .iter() +//! .map(|path| Song::new(path)) +//! .collect::, BlissError>>()?; +//! +//! // Assuming there is a first song +//! let first_song = songs.first().unwrap().to_owned(); +//! +//! songs.sort_by_cached_key(|song| n32(first_song.distance(&song))); +//! println!( +//! "Playlist is: {:?}", +//! songs +//! .iter() +//! .map(|song| &song.path) +//! .collect::>() +//! ); +//! Ok(()) +//! } +//! ``` #![cfg_attr(feature = "bench", feature(test))] #![warn(missing_docs)] #![warn(missing_doc_code_examples)] @@ -35,13 +80,13 @@ extern crate num_cpus; extern crate serde; use thiserror::Error; -pub use song::Song; pub use library::Library; +pub use song::{Analysis, AnalysisIndex, Song}; const CHANNELS: u16 = 1; const SAMPLE_RATE: u32 = 22050; -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Clone, Debug, PartialEq)] /// Umbrella type for bliss error types pub enum BlissError { #[error("error happened while decoding file – {0}")] diff --git a/src/library.rs b/src/library.rs index 118d4ec..2d39978 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1,8 +1,7 @@ //! Module containing the Library trait, useful to get started to implement //! a plug-in for an audio player. use crate::{BlissError, Song}; -use log::{error, info, warn}; -use ndarray::{arr1, Array}; +use log::{debug, error, info}; use noisy_float::prelude::*; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; @@ -43,18 +42,16 @@ pub trait Library { first_song: Song, playlist_length: usize, ) -> Result, BlissError> { - let analysis_current_song = arr1(&first_song.analysis.to_vec()); + let analysis_current_song = first_song.analysis; let mut songs = self.get_stored_songs()?; - let m = Array::eye(first_song.analysis.len()); - songs.sort_by_cached_key(|song| { - n32((arr1(&song.analysis) - &analysis_current_song) - .dot(&m) - .dot(&(arr1(&song.analysis) - &analysis_current_song))) - }); - Ok(songs + songs.sort_by_cached_key(|song| n32(analysis_current_song.distance(&song.analysis))); + + let playlist = songs .into_iter() .take(playlist_length) - .collect::>()) + .collect::>(); + debug!("Playlist created: {:?}", playlist); + Ok(playlist) } /// Analyze and store songs in `paths`, using `store_song` and @@ -103,11 +100,14 @@ pub trait Library { info!("Analyzed and stored song '{}' successfully.", song.path) } Err(e) => { - self.store_error_song(path.to_string(), e) + self.store_error_song(path.to_string(), e.to_owned()) .unwrap_or_else(|e| { error!("Error while storing errored song '{}': {}", path, e) }); - warn!("Analysis of song '{}' failed. Storing error.", path) + error!( + "Analysis of song '{}': {} failed. Error has been stored.", + path, e + ) } } } @@ -134,6 +134,7 @@ pub trait Library { #[cfg(test)] mod test { use super::*; + use crate::song::Analysis; #[derive(Default)] struct TestLibrary { @@ -247,7 +248,7 @@ mod test { let test_library = FailingLibrary {}; let song = Song { path: String::from("path-to-first"), - analysis: vec![0., 0., 0.], + analysis: Analysis::new([0.; 20]), ..Default::default() }; @@ -304,11 +305,6 @@ mod test { String::from("./data/white_noise.flac"), ], ); - - test_library - .internal_storage - .iter() - .for_each(|x| assert!(x.analysis.len() > 0)); } #[test] @@ -316,25 +312,25 @@ mod test { let mut test_library = TestLibrary::default(); let first_song = Song { path: String::from("path-to-first"), - analysis: vec![0., 0., 0.], + analysis: Analysis::new([0.; 20]), ..Default::default() }; let second_song = Song { path: String::from("path-to-second"), - analysis: vec![0.1, 0., 0.], + analysis: Analysis::new([0.1; 20]), ..Default::default() }; let third_song = Song { path: String::from("path-to-third"), - analysis: vec![10., 11., 10.], + analysis: Analysis::new([10.; 20]), ..Default::default() }; let fourth_song = Song { path: String::from("path-to-fourth"), - analysis: vec![20., 21., 20.], + analysis: Analysis::new([20.; 20]), ..Default::default() }; @@ -355,19 +351,19 @@ mod test { let mut test_library = TestLibrary::default(); let first_song = Song { path: String::from("path-to-first"), - analysis: vec![0., 0., 0.], + analysis: Analysis::new([0.; 20]), ..Default::default() }; let second_song = Song { path: String::from("path-to-second"), - analysis: vec![0.1, 0., 0.], + analysis: Analysis::new([0.1; 20]), ..Default::default() }; let third_song = Song { path: String::from("path-to-third"), - analysis: vec![10., 11., 10.], + analysis: Analysis::new([10.; 20]), ..Default::default() }; diff --git a/src/song.rs b/src/song.rs index 9e58a55..b51abd5 100644 --- a/src/song.rs +++ b/src/song.rs @@ -19,6 +19,7 @@ use crate::temporal::BPMDesc; use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc}; use crate::{BlissError, SAMPLE_RATE}; use ::log::warn; +use core::ops::Index; use crossbeam::thread; use ffmpeg_next::codec::threading::{Config, Type as ThreadingType}; use ffmpeg_next::software::resampling::context::Context; @@ -30,10 +31,14 @@ 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 ndarray::{arr1, Array}; +use ndarray::{arr1, Array, Array1}; +use std::convert::TryInto; +use std::fmt; use std::sync::mpsc; use std::sync::mpsc::Receiver; use std::thread as std_thread; +use strum::{EnumCount, IntoEnumIterator}; +use strum_macros::{EnumCount, EnumIter}; #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Default, Debug, PartialEq, Clone)] @@ -52,14 +57,126 @@ pub struct Song { pub track_number: String, /// Song's genre, read from the metadata (`""` if empty) pub genre: String, - /// Vec containing analysis, in order: tempo, zero-crossing rate, - /// mean spectral centroid, std deviation spectral centroid, - /// mean spectral rolloff, std deviation spectral rolloff - /// mean spectral_flatness, std deviation spectral flatness, - /// mean loudness, std deviation loudness, chroma interval feature 1 to 10. + /// bliss analysis results + pub analysis: Analysis, +} + +#[derive(Debug, EnumIter, EnumCount)] +/// Indexes different fields of an [Analysis](Song::analysis). +/// +/// * Example: +/// ```no_run +/// use bliss_audio::{AnalysisIndex, BlissError, Song}; +/// +/// fn main() -> Result<(), BlissError> { +/// let song = Song::new("path/to/song")?; +/// println!("{}", song.analysis[AnalysisIndex::Tempo]); +/// Ok(()) +/// } +/// ``` +/// +/// Prints the tempo value of an analysis. +/// +/// Note that this should mostly be used for debugging / distance metric +/// customization purposes. +#[allow(missing_docs)] +pub enum AnalysisIndex { + Tempo, + Zcr, + MeanSpectralCentroid, + StdDeviationSpectralCentroid, + MeanSpectralRolloff, + StdDeviationSpectralRolloff, + MeanSpectralFlatness, + StdDeviationSpectralFlatness, + MeanLoudness, + StdDeviationLoudness, + Chroma1, + Chroma2, + Chroma3, + Chroma4, + Chroma5, + Chroma6, + Chroma7, + Chroma8, + Chroma9, + Chroma10, +} +const NUMBER_FEATURES: usize = AnalysisIndex::COUNT; + +#[derive(Default, PartialEq, Clone, Copy)] +/// Object holding the results of the song's analysis. +/// +/// Only use it if you want to have an in-depth look of what is +/// happening behind the scene, or make a distance metric yourself. +/// +/// Under the hood, it is just an array of f32 holding different numeric +/// features. +/// +/// For more info on the different features, build the +/// documentation with private items included using +/// `cargo doc --document-private-items`, and / or read up +/// [this document](https://lelele.io/thesis.pdf), that contains a description +/// on most of the features, except the chroma ones, which are documented +/// directly in this code. +pub struct Analysis { + internal_analysis: [f32; NUMBER_FEATURES], +} + +impl Index for Analysis { + type Output = f32; + + fn index(&self, index: AnalysisIndex) -> &f32 { + &self.internal_analysis[index as usize] + } +} + +impl fmt::Debug for Analysis { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug_struct = f.debug_struct("Analysis"); + for feature in AnalysisIndex::iter() { + debug_struct.field(&format!("{:?}", feature), &self[feature]); + } + debug_struct.finish()?; + f.write_str(&format!(" /* {:?} */", &self.to_vec())) + } +} + +impl Analysis { + pub(crate) fn new(analysis: [f32; NUMBER_FEATURES]) -> Analysis { + Analysis { + internal_analysis: analysis, + } + } + + /// Return an ndarray `arr1`. /// - /// All the numbers are between -1 and 1. - pub analysis: Vec, + /// Particularly useful if you want to make a custom distance metric. + pub fn to_arr1(&self) -> Array1 { + arr1(&self.internal_analysis) + } + + #[allow(dead_code)] + pub(crate) fn to_vec(&self) -> Vec { + self.internal_analysis.to_vec() + } + + /// Return the [euclidean + /// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions) + /// between two analysis. + /// + /// Note that it is usually easier to just use [`song.distance(song2)`](Song::distance) + /// (which calls this function in turn). + pub fn distance(&self, other: &Self) -> f32 { + let a1 = self.to_arr1(); + let a2 = other.to_arr1(); + // Could be any square symmetric positive semi-definite matrix; + // just no metric learning has been done yet. + // See https://lelele.io/thesis.pdf chapter 4. + let m = Array::eye(NUMBER_FEATURES); + + (self.to_arr1() - &a2).dot(&m).dot(&(&a1 - &a2)).sqrt() + } } impl Song { @@ -71,21 +188,14 @@ impl Song { /// (e.g. if song1.distance(song2) < song1.distance(song3), then song1 is /// closer to song2 than it is to song3. pub fn distance(&self, other: &Self) -> f32 { - let a1 = arr1(&self.analysis.to_vec()); - let a2 = arr1(&other.analysis.to_vec()); - // Could be any square symmetric positive semi-definite matrix; - // just no metric learning has been done yet. - // See https://lelele.io/thesis.pdf chapter 4. - let m = Array::eye(self.analysis.len()); - - (arr1(&self.analysis) - &a2).dot(&m).dot(&(&a1 - &a2)) + self.analysis.distance(&other.analysis) } /// Returns a decoded Song given a file path, or an error if the song /// could not be analyzed for some reason. /// /// # Arguments - /// + /// /// * `path` - A string holding a valid file path to a valid audio file. /// /// # Errors @@ -122,7 +232,7 @@ impl Song { * Useful in the rare cases where the full song is not * completely available. **/ - fn analyse(sample_array: Vec) -> Result, BlissError> { + fn analyse(sample_array: Vec) -> Result { let largest_window = vec![ BPMDesc::WINDOW_SIZE, ChromaDesc::WINDOW_SIZE, @@ -207,7 +317,14 @@ impl Song { result.extend_from_slice(&flatness); result.extend_from_slice(&loudness); result.extend_from_slice(&chroma); - Ok(result) + let array: [f32; NUMBER_FEATURES] = result.try_into().map_err(|_| { + BlissError::AnalysisError( + "Too many or too little features were provided at the end of + the analysis." + .to_string(), + ) + })?; + Ok(Analysis::new(array)) }) .unwrap() } @@ -226,9 +343,7 @@ impl Song { let stream = format .streams() .find(|s| s.codec().medium() == ffmpeg::media::Type::Audio) - .ok_or_else(|| BlissError::DecodingError(String::from( - "No audio stream found.", - )))?; + .ok_or_else(|| BlissError::DecodingError(String::from("No audio stream found.")))?; stream.codec().set_threading(Config { kind: ThreadingType::Frame, count: 0, @@ -477,7 +592,7 @@ mod tests { -0.9820945, -0.95968974, ]; - for (x, y) in song.analysis.iter().zip(expected_analysis) { + for (x, y) in song.analysis.to_vec().iter().zip(expected_analysis) { assert!(0.01 > (x - y).abs()); } } @@ -582,48 +697,28 @@ mod tests { #[test] fn test_analysis_distance() { let mut a = Song::default(); - a.analysis = vec![ - 0.37860596, - -0.75483, - -0.85036564, - -0.6326486, - -0.77610075, - 0.27126348, - -1., - 0., - 1., - ]; + a.analysis = Analysis::new([ + 0.16391512, 0.11326739, 0.96868552, 0.8353934, 0.49867523, 0.76532606, 0.63448005, + 0.82506196, 0.71457147, 0.62395476, 0.69680329, 0.9855766, 0.41369333, 0.13900452, + 0.68001012, 0.11029723, 0.97192943, 0.57727861, 0.07994821, 0.88993185, + ]); let mut b = Song::default(); - b.analysis = vec![ - 0.31255, - 0.15483, - -0.15036564, - -0.0326486, - -0.87610075, - -0.27126348, - 1., - 0., - 1., - ]; - assert_eq!(a.distance(&b), 5.986180) + b.analysis = Analysis::new([ + 0.5075758, 0.36440256, 0.28888011, 0.43032829, 0.62387977, 0.61894916, 0.99676086, + 0.11913155, 0.00640396, 0.15943407, 0.33829514, 0.34947174, 0.82927523, 0.18987604, + 0.54437275, 0.22076826, 0.91232151, 0.29233168, 0.32846024, 0.04522147, + ]); + assert_eq!(a.distance(&b), 1.9469079) } #[test] fn test_analysis_distance_indiscernible() { let mut a = Song::default(); - a.analysis = vec![ - 0.37860596, - -0.75483, - -0.85036564, - -0.6326486, - -0.77610075, - 0.27126348, - -1., - 0., - 1., - ]; - + a.analysis = Analysis::new([ + 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18., 19., + 20., + ]); assert_eq!(a.distance(&a), 0.) } @@ -640,6 +735,33 @@ mod tests { BlissError::DecodingError(String::from("No audio stream found.")), ); } + + #[test] + fn test_index_analysis() { + let song = Song::new("data/s16_mono_22_5kHz.flac").unwrap(); + assert_eq!(song.analysis[AnalysisIndex::Tempo], 0.3846389); + assert_eq!(song.analysis[AnalysisIndex::Chroma10], -0.95968974); + } + + #[test] + fn test_debug_analysis() { + let song = Song::new("data/s16_mono_22_5kHz.flac").unwrap(); + assert_eq!( + "Analysis { Tempo: 0.3846389, Zcr: -0.849141, MeanSpectralCentroid: \ + -0.75481045, StdDeviationSpectralCentroid: -0.8790748, MeanSpectralR\ + olloff: -0.63258266, StdDeviationSpectralRolloff: -0.7258959, MeanSp\ + ectralFlatness: -0.7757379, StdDeviationSpectralFlatness: -0.8146726\ + , MeanLoudness: 0.2716726, StdDeviationLoudness: 0.25779057, Chroma1\ + : -0.35661936, Chroma2: -0.63578653, Chroma3: -0.29593682, Chroma4: \ + 0.06421304, Chroma5: 0.21852458, Chroma6: -0.581239, Chroma7: -0.946\ + 6835, Chroma8: -0.9481153, Chroma9: -0.9820945, Chroma10: -0.95968974 } \ + /* [0.3846389, -0.849141, -0.75481045, -0.8790748, -0.63258266, -0.\ + 7258959, -0.7757379, -0.8146726, 0.2716726, 0.25779057, -0.35661936, \ + -0.63578653, -0.29593682, 0.06421304, 0.21852458, -0.581239, -0.946\ + 6835, -0.9481153, -0.9820945, -0.95968974] */", + format!("{:?}", song.analysis), + ); + } } #[cfg(all(feature = "bench", test))]