Merge pull request #16 from Polochon-street/add-cosine-distance
Add cosine distance and formatter
This commit is contained in:
commit
ff500851c0
9 changed files with 300 additions and 68 deletions
|
@ -1,5 +1,11 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## bliss 0.3.1
|
||||||
|
* Show error message when song storage fails in the Library trait.
|
||||||
|
* Added a `distance` module containing euclidean and cosine distance.
|
||||||
|
* Added various custom_distance functions to avoid being limited to the
|
||||||
|
euclidean distance only.
|
||||||
|
|
||||||
## bliss 0.3.0
|
## bliss 0.3.0
|
||||||
* Changed `Song.path` from `String` to `PathBuf`.
|
* Changed `Song.path` from `String` to `PathBuf`.
|
||||||
* Made `Song` metadata (artist, album, etc) `Option`s.
|
* Made `Song` metadata (artist, album, etc) `Option`s.
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -75,7 +75,7 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bliss-audio"
|
name = "bliss-audio"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bliss-audio-aubio-rs",
|
"bliss-audio-aubio-rs",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bliss-audio"
|
name = "bliss-audio"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
|
@ -556,8 +556,8 @@ mod bench {
|
||||||
use ndarray::{arr2, Array1, Array2};
|
use ndarray::{arr2, Array1, Array2};
|
||||||
use ndarray_npy::ReadNpyExt;
|
use ndarray_npy::ReadNpyExt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use test::Bencher;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_estimate_tuning(b: &mut Bencher) {
|
fn bench_estimate_tuning(b: &mut Bencher) {
|
||||||
|
|
75
src/distance.rs
Normal file
75
src/distance.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
//! Module containing various distance metric functions.
|
||||||
|
//!
|
||||||
|
//! All of these functions are intended to be used with the
|
||||||
|
//! [custom_distance](Song::custom_distance) method, or with
|
||||||
|
//! [playlist_from_songs_custom_distance](Library::playlist_from_song_custom_distance).
|
||||||
|
//!
|
||||||
|
//! They will yield different styles of playlists, so don't hesitate to
|
||||||
|
//! experiment with them if the default (euclidean distance for now) doesn't
|
||||||
|
//! suit you.
|
||||||
|
use crate::NUMBER_FEATURES;
|
||||||
|
#[cfg(doc)]
|
||||||
|
use crate::{Library, Song};
|
||||||
|
use ndarray::{Array, Array1};
|
||||||
|
|
||||||
|
/// Convenience trait for user-defined distance metrics.
|
||||||
|
pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
|
||||||
|
impl<F> DistanceMetric for F where F: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
|
||||||
|
|
||||||
|
/// Return the [euclidean
|
||||||
|
/// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions)
|
||||||
|
/// between two vectors.
|
||||||
|
pub fn euclidean_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
(a - b).dot(&m).dot(&(a - b)).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the [cosine
|
||||||
|
/// distance](https://en.wikipedia.org/wiki/Cosine_similarity#Angular_distance_and_similarity)
|
||||||
|
/// between two vectors.
|
||||||
|
pub fn cosine_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||||||
|
let similarity = a.dot(b) / (a.dot(a).sqrt() * b.dot(b).sqrt());
|
||||||
|
1. - similarity
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use ndarray::arr1;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_euclidean_distance() {
|
||||||
|
let a = arr1(&[
|
||||||
|
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0.,
|
||||||
|
]);
|
||||||
|
let b = arr1(&[
|
||||||
|
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
|
||||||
|
]);
|
||||||
|
assert_eq!(euclidean_distance(&a, &b), 4.242640687119285);
|
||||||
|
|
||||||
|
let a = arr1(&[0.5; 20]);
|
||||||
|
let b = arr1(&[0.5; 20]);
|
||||||
|
assert_eq!(euclidean_distance(&a, &b), 0.);
|
||||||
|
assert_eq!(euclidean_distance(&a, &b), 0.);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cosine_distance() {
|
||||||
|
let a = arr1(&[
|
||||||
|
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0.,
|
||||||
|
]);
|
||||||
|
let b = arr1(&[
|
||||||
|
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
|
||||||
|
]);
|
||||||
|
assert_eq!(cosine_distance(&a, &b), 0.7705842661294382);
|
||||||
|
|
||||||
|
let a = arr1(&[0.5; 20]);
|
||||||
|
let b = arr1(&[0.5; 20]);
|
||||||
|
assert_eq!(cosine_distance(&a, &b), 0.);
|
||||||
|
assert_eq!(cosine_distance(&a, &b), 0.);
|
||||||
|
}
|
||||||
|
}
|
17
src/lib.rs
17
src/lib.rs
|
@ -25,7 +25,7 @@
|
||||||
//! ## Analyze & compute the distance between two songs
|
//! ## Analyze & compute the distance between two songs
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use bliss_audio::{BlissResult, Song};
|
//! use bliss_audio::{BlissResult, Song};
|
||||||
//!
|
//!
|
||||||
//! fn main() -> BlissResult<()> {
|
//! fn main() -> BlissResult<()> {
|
||||||
//! let song1 = Song::new("/path/to/song1")?;
|
//! let song1 = Song::new("/path/to/song1")?;
|
||||||
//! let song2 = Song::new("/path/to/song2")?;
|
//! let song2 = Song::new("/path/to/song2")?;
|
||||||
|
@ -34,19 +34,19 @@
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ### Make a playlist from a song
|
//! ### Make a playlist from a song
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use bliss_audio::{BlissResult, Song};
|
//! use bliss_audio::{BlissResult, Song};
|
||||||
//! use noisy_float::prelude::n32;
|
//! use noisy_float::prelude::n32;
|
||||||
//!
|
//!
|
||||||
//! fn main() -> BlissResult<()> {
|
//! fn main() -> BlissResult<()> {
|
||||||
//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
|
//! let paths = vec!["/path/to/song1", "/path/to/song2", "/path/to/song3"];
|
||||||
//! let mut songs: Vec<Song> = paths
|
//! let mut songs: Vec<Song> = paths
|
||||||
//! .iter()
|
//! .iter()
|
||||||
//! .map(|path| Song::new(path))
|
//! .map(|path| Song::new(path))
|
||||||
//! .collect::<BlissResult<Vec<Song>>>()?;
|
//! .collect::<BlissResult<Vec<Song>>>()?;
|
||||||
//!
|
//!
|
||||||
//! // Assuming there is a first song
|
//! // Assuming there is a first song
|
||||||
//! let first_song = songs.first().unwrap().to_owned();
|
//! let first_song = songs.first().unwrap().to_owned();
|
||||||
//!
|
//!
|
||||||
|
@ -65,6 +65,7 @@
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![warn(missing_doc_code_examples)]
|
#![warn(missing_doc_code_examples)]
|
||||||
mod chroma;
|
mod chroma;
|
||||||
|
pub mod distance;
|
||||||
mod library;
|
mod library;
|
||||||
mod misc;
|
mod misc;
|
||||||
mod song;
|
mod song;
|
||||||
|
@ -80,7 +81,7 @@ extern crate serde;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use library::Library;
|
pub use library::Library;
|
||||||
pub use song::{Analysis, AnalysisIndex, NUMBER_FEATURES, Song};
|
pub use song::{Analysis, AnalysisIndex, Song, NUMBER_FEATURES};
|
||||||
|
|
||||||
const CHANNELS: u16 = 1;
|
const CHANNELS: u16 = 1;
|
||||||
const SAMPLE_RATE: u32 = 22050;
|
const SAMPLE_RATE: u32 = 22050;
|
||||||
|
@ -178,7 +179,11 @@ mod tests {
|
||||||
|
|
||||||
let mut analysed_songs: Vec<String> = results
|
let mut analysed_songs: Vec<String> = results
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|x| x.as_ref().ok().map(|x| x.path.to_str().unwrap().to_string()))
|
.filter_map(|x| {
|
||||||
|
x.as_ref()
|
||||||
|
.ok()
|
||||||
|
.map(|x| x.path.to_str().unwrap().to_string())
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
analysed_songs.sort_by(|a, b| a.cmp(b));
|
analysed_songs.sort_by(|a, b| a.cmp(b));
|
||||||
|
|
||||||
|
|
136
src/library.rs
136
src/library.rs
|
@ -1,5 +1,8 @@
|
||||||
//! Module containing the Library trait, useful to get started to implement
|
//! Module containing the Library trait, useful to get started to implement
|
||||||
//! a plug-in for an audio player.
|
//! a plug-in for an audio player.
|
||||||
|
#[cfg(doc)]
|
||||||
|
use crate::distance;
|
||||||
|
use crate::distance::DistanceMetric;
|
||||||
use crate::{BlissError, BlissResult, Song};
|
use crate::{BlissError, BlissResult, Song};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use noisy_float::prelude::*;
|
use noisy_float::prelude::*;
|
||||||
|
@ -42,15 +45,71 @@ pub trait Library {
|
||||||
first_song: Song,
|
first_song: Song,
|
||||||
playlist_length: usize,
|
playlist_length: usize,
|
||||||
) -> BlissResult<Vec<Song>> {
|
) -> BlissResult<Vec<Song>> {
|
||||||
let analysis_current_song = first_song.analysis;
|
|
||||||
let mut songs = self.get_stored_songs()?;
|
let mut songs = self.get_stored_songs()?;
|
||||||
songs.sort_by_cached_key(|song| n32(analysis_current_song.distance(&song.analysis)));
|
songs.sort_by_cached_key(|song| n32(first_song.distance(&song)));
|
||||||
|
|
||||||
let playlist = songs
|
let playlist = songs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.take(playlist_length)
|
.take(playlist_length)
|
||||||
.collect::<Vec<Song>>();
|
.collect::<Vec<Song>>();
|
||||||
debug!("Playlist created: {:?}", playlist);
|
debug!(
|
||||||
|
"Playlist created: {}",
|
||||||
|
playlist
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("{:?}", &s))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
|
Ok(playlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a list of songs that are similar to ``first_song``, using a
|
||||||
|
/// custom distance metric.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `first_song` - The song the playlist will be built from.
|
||||||
|
/// * `playlist_length` - The playlist length. If there are not enough
|
||||||
|
/// songs in the library, it will be truncated to the size of the library.
|
||||||
|
/// * `distance` - a user-supplied valid distance metric, either taken
|
||||||
|
/// from the [distance](distance) module, or made from scratch.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of `playlist_length` Songs, including `first_song`, that you
|
||||||
|
/// most likely want to plug in your audio player by using something like
|
||||||
|
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
|
||||||
|
///
|
||||||
|
/// # Custom distance example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ndarray::Array1;
|
||||||
|
///
|
||||||
|
/// fn manhattan_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||||||
|
/// (a - b).mapv(|x| x.abs()).sum()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn playlist_from_song_custom_distance(
|
||||||
|
&self,
|
||||||
|
first_song: Song,
|
||||||
|
playlist_length: usize,
|
||||||
|
distance: impl DistanceMetric,
|
||||||
|
) -> BlissResult<Vec<Song>> {
|
||||||
|
let mut songs = self.get_stored_songs()?;
|
||||||
|
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&song, &distance)));
|
||||||
|
|
||||||
|
let playlist = songs
|
||||||
|
.into_iter()
|
||||||
|
.take(playlist_length)
|
||||||
|
.collect::<Vec<Song>>();
|
||||||
|
debug!(
|
||||||
|
"Playlist created: {}",
|
||||||
|
playlist
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("{:?}", &s))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n"),
|
||||||
|
);
|
||||||
Ok(playlist)
|
Ok(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,9 +154,13 @@ pub trait Library {
|
||||||
// A storage fail should just warn the user, but not abort the whole process
|
// A storage fail should just warn the user, but not abort the whole process
|
||||||
match song {
|
match song {
|
||||||
Ok(song) => {
|
Ok(song) => {
|
||||||
self.store_song(&song)
|
self.store_song(&song).unwrap_or_else(|e| {
|
||||||
.unwrap_or_else(|_| error!("Error while storing song '{}'", song.path.display()));
|
error!("Error while storing song '{}': {}", song.path.display(), e)
|
||||||
info!("Analyzed and stored song '{}' successfully.", song.path.display())
|
});
|
||||||
|
info!(
|
||||||
|
"Analyzed and stored song '{}' successfully.",
|
||||||
|
song.path.display()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
self.store_error_song(path.to_string(), e.to_owned())
|
self.store_error_song(path.to_string(), e.to_owned())
|
||||||
|
@ -135,6 +198,7 @@ pub trait Library {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::song::Analysis;
|
use crate::song::Analysis;
|
||||||
|
use ndarray::Array1;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -158,11 +222,7 @@ mod test {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_error_song(
|
fn store_error_song(&mut self, song_path: String, error: BlissError) -> BlissResult<()> {
|
||||||
&mut self,
|
|
||||||
song_path: String,
|
|
||||||
error: BlissError,
|
|
||||||
) -> BlissResult<()> {
|
|
||||||
self.failed_files.push((song_path, error.to_string()));
|
self.failed_files.push((song_path, error.to_string()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -221,11 +281,7 @@ mod test {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_error_song(
|
fn store_error_song(&mut self, song_path: String, error: BlissError) -> BlissResult<()> {
|
||||||
&mut self,
|
|
||||||
song_path: String,
|
|
||||||
error: BlissError,
|
|
||||||
) -> BlissResult<()> {
|
|
||||||
Err(BlissError::ProviderError(format!(
|
Err(BlissError::ProviderError(format!(
|
||||||
"Could not store errored song: {}, with error: {}",
|
"Could not store errored song: {}, with error: {}",
|
||||||
song_path, error
|
song_path, error
|
||||||
|
@ -384,4 +440,52 @@ mod test {
|
||||||
let mut test_library = TestLibrary::default();
|
let mut test_library = TestLibrary::default();
|
||||||
assert!(test_library.analyze_paths(vec![]).is_ok());
|
assert!(test_library.analyze_paths(vec![]).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn custom_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||||||
|
if a == b {
|
||||||
|
return 0.;
|
||||||
|
}
|
||||||
|
1. / (a.first().unwrap() - b.first().unwrap()).abs()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_playlist_from_song_custom_distance() {
|
||||||
|
let mut test_library = TestLibrary::default();
|
||||||
|
let first_song = Song {
|
||||||
|
path: Path::new("path-to-first").to_path_buf(),
|
||||||
|
analysis: Analysis::new([0.; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_song = Song {
|
||||||
|
path: Path::new("path-to-second").to_path_buf(),
|
||||||
|
analysis: Analysis::new([0.1; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let third_song = Song {
|
||||||
|
path: Path::new("path-to-third").to_path_buf(),
|
||||||
|
analysis: Analysis::new([10.; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let fourth_song = Song {
|
||||||
|
path: Path::new("path-to-fourth").to_path_buf(),
|
||||||
|
analysis: Analysis::new([20.; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
test_library.internal_storage = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![first_song.to_owned(), fourth_song, third_song],
|
||||||
|
test_library
|
||||||
|
.playlist_from_song_custom_distance(first_song, 3, custom_distance)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
122
src/song.rs
122
src/song.rs
|
@ -14,6 +14,7 @@ extern crate ndarray_npy;
|
||||||
|
|
||||||
use super::CHANNELS;
|
use super::CHANNELS;
|
||||||
use crate::chroma::ChromaDesc;
|
use crate::chroma::ChromaDesc;
|
||||||
|
use crate::distance::{euclidean_distance, DistanceMetric};
|
||||||
use crate::misc::LoudnessDesc;
|
use crate::misc::LoudnessDesc;
|
||||||
use crate::temporal::BPMDesc;
|
use crate::temporal::BPMDesc;
|
||||||
use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
|
use crate::timbral::{SpectralDesc, ZeroCrossingRateDesc};
|
||||||
|
@ -31,13 +32,13 @@ use ffmpeg_next::util::format::sample::{Sample, Type};
|
||||||
use ffmpeg_next::util::frame::audio::Audio;
|
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, Array, Array1};
|
use ndarray::{arr1, Array1};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::mpsc;
|
|
||||||
use std::sync::mpsc::Receiver;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::sync::mpsc::Receiver;
|
||||||
use std::thread as std_thread;
|
use std::thread as std_thread;
|
||||||
use strum::{EnumCount, IntoEnumIterator};
|
use strum::{EnumCount, IntoEnumIterator};
|
||||||
use strum_macros::{EnumCount, EnumIter};
|
use strum_macros::{EnumCount, EnumIter};
|
||||||
|
@ -173,34 +174,55 @@ impl Analysis {
|
||||||
self.internal_analysis.to_vec()
|
self.internal_analysis.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [euclidean
|
/// Compute distance between two analysis using a user-provided distance
|
||||||
/// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions)
|
/// metric. You most likely want to use `song.custom_distance` directly
|
||||||
/// between two analysis.
|
/// rather than this function.
|
||||||
///
|
///
|
||||||
/// Note that it is usually easier to just use [`song.distance(song2)`](Song::distance)
|
/// For this function to be integrated properly with the rest
|
||||||
/// (which calls this function in turn).
|
/// of bliss' parts, it should be a valid distance metric, i.e.:
|
||||||
pub fn distance(&self, other: &Self) -> f32 {
|
/// 1. For X, Y real vectors, d(X, Y) = 0 ⇔ X = Y
|
||||||
let a1 = self.to_arr1();
|
/// 2. For X, Y real vectors, d(X, Y) >= 0
|
||||||
let a2 = other.to_arr1();
|
/// 3. For X, Y real vectors, d(X, Y) = d(Y, X)
|
||||||
// Could be any square symmetric positive semi-definite matrix;
|
/// 4. For X, Y, Z real vectors d(X, Y) ≤ d(X + Z) + d(Z, Y)
|
||||||
// just no metric learning has been done yet.
|
///
|
||||||
// See https://lelele.io/thesis.pdf chapter 4.
|
/// Note that almost all distance metrics you will find obey these
|
||||||
let m = Array::eye(NUMBER_FEATURES);
|
/// properties, so don't sweat it too much.
|
||||||
|
pub fn custom_distance(&self, other: &Self, distance: impl DistanceMetric) -> f32 {
|
||||||
(self.to_arr1() - &a2).dot(&m).dot(&(&a1 - &a2)).sqrt()
|
distance(&self.to_arr1(), &other.to_arr1())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Song {
|
impl Song {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
/// Compute the distance between the current song and any given Song.
|
/// Compute the distance between the current song and any given
|
||||||
|
/// Song.
|
||||||
///
|
///
|
||||||
/// The smaller the number, the closer the songs; usually more useful
|
/// The smaller the number, the closer the songs; usually more useful
|
||||||
/// if compared between several songs
|
/// if compared between several songs
|
||||||
/// (e.g. if song1.distance(song2) < song1.distance(song3), then song1 is
|
/// (e.g. if song1.distance(song2) < song1.distance(song3), then song1 is
|
||||||
/// closer to song2 than it is to song3.
|
/// closer to song2 than it is to song3.
|
||||||
|
///
|
||||||
|
/// Currently uses the euclidean distance, but this can change in an
|
||||||
|
/// upcoming release if another metric performs better.
|
||||||
pub fn distance(&self, other: &Self) -> f32 {
|
pub fn distance(&self, other: &Self) -> f32 {
|
||||||
self.analysis.distance(&other.analysis)
|
self.analysis
|
||||||
|
.custom_distance(&other.analysis, euclidean_distance)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute distance between two songs using a user-provided distance
|
||||||
|
/// metric.
|
||||||
|
///
|
||||||
|
/// For this function to be integrated properly with the rest
|
||||||
|
/// of bliss' parts, it should be a valid distance metric, i.e.:
|
||||||
|
/// 1. For X, Y real vectors, d(X, Y) = 0 ⇔ X = Y
|
||||||
|
/// 2. For X, Y real vectors, d(X, Y) >= 0
|
||||||
|
/// 3. For X, Y real vectors, d(X, Y) = d(Y, X)
|
||||||
|
/// 4. For X, Y, Z real vectors d(X, Y) ≤ d(X + Z) + d(Z, Y)
|
||||||
|
///
|
||||||
|
/// Note that almost all distance metrics you will find obey these
|
||||||
|
/// properties, so don't sweat it too much.
|
||||||
|
pub fn custom_distance(&self, other: &Self, distance: impl DistanceMetric) -> f32 {
|
||||||
|
self.analysis.custom_distance(&other.analysis, distance)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a decoded Song given a file path, or an error if the song
|
/// Returns a decoded Song given a file path, or an error if the song
|
||||||
|
@ -261,25 +283,23 @@ impl Song {
|
||||||
}
|
}
|
||||||
|
|
||||||
thread::scope(|s| {
|
thread::scope(|s| {
|
||||||
let child_tempo: thread::ScopedJoinHandle<'_, BlissResult<f32>> =
|
let child_tempo: thread::ScopedJoinHandle<'_, BlissResult<f32>> = s.spawn(|_| {
|
||||||
s.spawn(|_| {
|
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
|
||||||
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
|
let windows = sample_array
|
||||||
let windows = sample_array
|
.windows(BPMDesc::WINDOW_SIZE)
|
||||||
.windows(BPMDesc::WINDOW_SIZE)
|
.step_by(BPMDesc::HOP_SIZE);
|
||||||
.step_by(BPMDesc::HOP_SIZE);
|
|
||||||
|
|
||||||
for window in windows {
|
for window in windows {
|
||||||
tempo_desc.do_(&window)?;
|
tempo_desc.do_(&window)?;
|
||||||
}
|
}
|
||||||
Ok(tempo_desc.get_value())
|
Ok(tempo_desc.get_value())
|
||||||
});
|
});
|
||||||
|
|
||||||
let child_chroma: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> =
|
let child_chroma: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> = s.spawn(|_| {
|
||||||
s.spawn(|_| {
|
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
|
||||||
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
|
chroma_desc.do_(&sample_array)?;
|
||||||
chroma_desc.do_(&sample_array)?;
|
Ok(chroma_desc.get_values())
|
||||||
Ok(chroma_desc.get_values())
|
});
|
||||||
});
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
let child_timbral: thread::ScopedJoinHandle<
|
let child_timbral: thread::ScopedJoinHandle<
|
||||||
|
@ -305,8 +325,8 @@ impl Song {
|
||||||
Ok(zcr_desc.get_value())
|
Ok(zcr_desc.get_value())
|
||||||
});
|
});
|
||||||
|
|
||||||
let child_loudness: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> = s
|
let child_loudness: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> =
|
||||||
.spawn(|_| {
|
s.spawn(|_| {
|
||||||
let mut loudness_desc = LoudnessDesc::default();
|
let mut loudness_desc = LoudnessDesc::default();
|
||||||
let windows = sample_array.chunks(LoudnessDesc::WINDOW_SIZE);
|
let windows = sample_array.chunks(LoudnessDesc::WINDOW_SIZE);
|
||||||
|
|
||||||
|
@ -390,7 +410,6 @@ impl Song {
|
||||||
"" => None,
|
"" => None,
|
||||||
a => Some(a.to_string()),
|
a => Some(a.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
if let Some(album) = format.metadata().get("album") {
|
if let Some(album) = format.metadata().get("album") {
|
||||||
song.album = match album {
|
song.album = match album {
|
||||||
|
@ -653,7 +672,7 @@ mod tests {
|
||||||
fn test_empty_tags() {
|
fn test_empty_tags() {
|
||||||
let song = Song::decode(Path::new("data/no_tags.flac")).unwrap();
|
let song = Song::decode(Path::new("data/no_tags.flac")).unwrap();
|
||||||
assert_eq!(song.artist, None);
|
assert_eq!(song.artist, None);
|
||||||
assert_eq!(song.title, None);
|
assert_eq!(song.title, None);
|
||||||
assert_eq!(song.album, None);
|
assert_eq!(song.album, None);
|
||||||
assert_eq!(song.track_number, None);
|
assert_eq!(song.track_number, None);
|
||||||
assert_eq!(song.genre, None);
|
assert_eq!(song.genre, None);
|
||||||
|
@ -804,14 +823,35 @@ mod tests {
|
||||||
format!("{:?}", song.analysis),
|
format!("{:?}", song.analysis),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dummy_distance(_: &Array1<f32>, _: &Array1<f32>) -> f32 {
|
||||||
|
0.
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_custom_distance() {
|
||||||
|
let mut a = Song::default();
|
||||||
|
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 = 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.custom_distance(&b, dummy_distance), 0.);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "bench", test))]
|
#[cfg(all(feature = "bench", test))]
|
||||||
mod bench {
|
mod bench {
|
||||||
extern crate test;
|
extern crate test;
|
||||||
use crate::Song;
|
use crate::Song;
|
||||||
use test::Bencher;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_resample_multi(b: &mut Bencher) {
|
fn bench_resample_multi(b: &mut Bencher) {
|
||||||
|
|
|
@ -525,8 +525,8 @@ mod bench {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::Song;
|
use crate::Song;
|
||||||
use ndarray::Array;
|
use ndarray::Array;
|
||||||
use test::Bencher;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_convolve(b: &mut Bencher) {
|
fn bench_convolve(b: &mut Bencher) {
|
||||||
|
@ -540,7 +540,9 @@ mod bench {
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
fn bench_compute_stft(b: &mut Bencher) {
|
fn bench_compute_stft(b: &mut Bencher) {
|
||||||
let signal = Song::decode(Path::new("data/piano.flac")).unwrap().sample_array;
|
let signal = Song::decode(Path::new("data/piano.flac"))
|
||||||
|
.unwrap()
|
||||||
|
.sample_array;
|
||||||
|
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
stft(&signal, 2048, 512);
|
stft(&signal, 2048, 512);
|
||||||
|
|
Loading…
Reference in a new issue