Compare commits

...

1 commit

Author SHA1 Message Date
Polochon-street b0a96e257e Change analysis from Vec<f32> to Analysis 2021-06-05 19:23:47 +02:00
7 changed files with 514 additions and 107 deletions

View file

@ -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.

270
Cargo.lock generated
View file

@ -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",
]

View file

@ -1,13 +1,13 @@
[package]
name = "bliss-audio"
version = "0.1.3"
version = "0.2.0"
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
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"] }

View file

@ -10,7 +10,7 @@ fn main() {
let args: Vec<String> = 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),
}
}

View file

@ -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<Song> = paths
//! .iter()
//! .map(|path| Song::new(path))
//! .collect::<Result<Vec<Song>, 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::<Vec<&String>>()
//! );
//! 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}")]

View file

@ -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<Vec<Song>, 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::<Vec<Song>>())
.collect::<Vec<Song>>();
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()
};

View file

@ -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<AnalysisIndex> 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<f32>,
/// Particularly useful if you want to make a custom distance metric.
pub fn to_arr1(&self) -> Array1<f32> {
arr1(&self.internal_analysis)
}
#[allow(dead_code)]
pub(crate) fn to_vec(&self) -> Vec<f32> {
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<f32>) -> Result<Vec<f32>, BlissError> {
fn analyse(sample_array: Vec<f32>) -> Result<Analysis, BlissError> {
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))]