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
|
||||
|
||||
## 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
|
||||
* Changed `Song.path` from `String` to `PathBuf`.
|
||||
* Made `Song` metadata (artist, album, etc) `Option`s.
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -75,7 +75,7 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
|||
|
||||
[[package]]
|
||||
name = "bliss-audio"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"bliss-audio-aubio-rs",
|
||||
"crossbeam",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "bliss-audio"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-only"
|
||||
|
|
|
@ -556,8 +556,8 @@ mod bench {
|
|||
use ndarray::{arr2, Array1, Array2};
|
||||
use ndarray_npy::ReadNpyExt;
|
||||
use std::fs::File;
|
||||
use test::Bencher;
|
||||
use std::path::Path;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
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
|
||||
//! ```no_run
|
||||
//! use bliss_audio::{BlissResult, Song};
|
||||
//!
|
||||
//!
|
||||
//! fn main() -> BlissResult<()> {
|
||||
//! let song1 = Song::new("/path/to/song1")?;
|
||||
//! let song2 = Song::new("/path/to/song2")?;
|
||||
|
@ -34,19 +34,19 @@
|
|||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//!
|
||||
//! ### Make a playlist from a song
|
||||
//! ```no_run
|
||||
//! use bliss_audio::{BlissResult, Song};
|
||||
//! use noisy_float::prelude::n32;
|
||||
//!
|
||||
//!
|
||||
//! fn main() -> BlissResult<()> {
|
||||
//! 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::<BlissResult<Vec<Song>>>()?;
|
||||
//!
|
||||
//!
|
||||
//! // Assuming there is a first song
|
||||
//! let first_song = songs.first().unwrap().to_owned();
|
||||
//!
|
||||
|
@ -65,6 +65,7 @@
|
|||
#![warn(missing_docs)]
|
||||
#![warn(missing_doc_code_examples)]
|
||||
mod chroma;
|
||||
pub mod distance;
|
||||
mod library;
|
||||
mod misc;
|
||||
mod song;
|
||||
|
@ -80,7 +81,7 @@ extern crate serde;
|
|||
use thiserror::Error;
|
||||
|
||||
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 SAMPLE_RATE: u32 = 22050;
|
||||
|
@ -178,7 +179,11 @@ mod tests {
|
|||
|
||||
let mut analysed_songs: Vec<String> = results
|
||||
.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();
|
||||
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
|
||||
//! a plug-in for an audio player.
|
||||
#[cfg(doc)]
|
||||
use crate::distance;
|
||||
use crate::distance::DistanceMetric;
|
||||
use crate::{BlissError, BlissResult, Song};
|
||||
use log::{debug, error, info};
|
||||
use noisy_float::prelude::*;
|
||||
|
@ -42,15 +45,71 @@ pub trait Library {
|
|||
first_song: Song,
|
||||
playlist_length: usize,
|
||||
) -> BlissResult<Vec<Song>> {
|
||||
let analysis_current_song = first_song.analysis;
|
||||
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
|
||||
.into_iter()
|
||||
.take(playlist_length)
|
||||
.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)
|
||||
}
|
||||
|
||||
|
@ -95,9 +154,13 @@ pub trait Library {
|
|||
// A storage fail should just warn the user, but not abort the whole process
|
||||
match song {
|
||||
Ok(song) => {
|
||||
self.store_song(&song)
|
||||
.unwrap_or_else(|_| error!("Error while storing song '{}'", song.path.display()));
|
||||
info!("Analyzed and stored song '{}' successfully.", song.path.display())
|
||||
self.store_song(&song).unwrap_or_else(|e| {
|
||||
error!("Error while storing song '{}': {}", song.path.display(), e)
|
||||
});
|
||||
info!(
|
||||
"Analyzed and stored song '{}' successfully.",
|
||||
song.path.display()
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
self.store_error_song(path.to_string(), e.to_owned())
|
||||
|
@ -135,6 +198,7 @@ pub trait Library {
|
|||
mod test {
|
||||
use super::*;
|
||||
use crate::song::Analysis;
|
||||
use ndarray::Array1;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -158,11 +222,7 @@ mod test {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn store_error_song(
|
||||
&mut self,
|
||||
song_path: String,
|
||||
error: BlissError,
|
||||
) -> BlissResult<()> {
|
||||
fn store_error_song(&mut self, song_path: String, error: BlissError) -> BlissResult<()> {
|
||||
self.failed_files.push((song_path, error.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
@ -221,11 +281,7 @@ mod test {
|
|||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn store_error_song(
|
||||
&mut self,
|
||||
song_path: String,
|
||||
error: BlissError,
|
||||
) -> BlissResult<()> {
|
||||
fn store_error_song(&mut self, song_path: String, error: BlissError) -> BlissResult<()> {
|
||||
Err(BlissError::ProviderError(format!(
|
||||
"Could not store errored song: {}, with error: {}",
|
||||
song_path, error
|
||||
|
@ -384,4 +440,52 @@ mod test {
|
|||
let mut test_library = TestLibrary::default();
|
||||
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 crate::chroma::ChromaDesc;
|
||||
use crate::distance::{euclidean_distance, DistanceMetric};
|
||||
use crate::misc::LoudnessDesc;
|
||||
use crate::temporal::BPMDesc;
|
||||
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::log;
|
||||
use ffmpeg_next::util::log::level::Level;
|
||||
use ndarray::{arr1, Array, Array1};
|
||||
use ndarray::{arr1, Array1};
|
||||
use std::convert::TryInto;
|
||||
use std::fmt;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::thread as std_thread;
|
||||
use strum::{EnumCount, IntoEnumIterator};
|
||||
use strum_macros::{EnumCount, EnumIter};
|
||||
|
@ -173,34 +174,55 @@ impl Analysis {
|
|||
self.internal_analysis.to_vec()
|
||||
}
|
||||
|
||||
/// Return the [euclidean
|
||||
/// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions)
|
||||
/// between two analysis.
|
||||
/// Compute distance between two analysis using a user-provided distance
|
||||
/// metric. You most likely want to use `song.custom_distance` directly
|
||||
/// rather than this function.
|
||||
///
|
||||
/// 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()
|
||||
/// 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 {
|
||||
distance(&self.to_arr1(), &other.to_arr1())
|
||||
}
|
||||
}
|
||||
|
||||
impl Song {
|
||||
#[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
|
||||
/// if compared between several songs
|
||||
/// (e.g. if song1.distance(song2) < song1.distance(song3), then song1 is
|
||||
/// 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 {
|
||||
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
|
||||
|
@ -261,25 +283,23 @@ impl Song {
|
|||
}
|
||||
|
||||
thread::scope(|s| {
|
||||
let child_tempo: thread::ScopedJoinHandle<'_, BlissResult<f32>> =
|
||||
s.spawn(|_| {
|
||||
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
|
||||
let windows = sample_array
|
||||
.windows(BPMDesc::WINDOW_SIZE)
|
||||
.step_by(BPMDesc::HOP_SIZE);
|
||||
let child_tempo: thread::ScopedJoinHandle<'_, BlissResult<f32>> = s.spawn(|_| {
|
||||
let mut tempo_desc = BPMDesc::new(SAMPLE_RATE)?;
|
||||
let windows = sample_array
|
||||
.windows(BPMDesc::WINDOW_SIZE)
|
||||
.step_by(BPMDesc::HOP_SIZE);
|
||||
|
||||
for window in windows {
|
||||
tempo_desc.do_(&window)?;
|
||||
}
|
||||
Ok(tempo_desc.get_value())
|
||||
});
|
||||
for window in windows {
|
||||
tempo_desc.do_(&window)?;
|
||||
}
|
||||
Ok(tempo_desc.get_value())
|
||||
});
|
||||
|
||||
let child_chroma: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> =
|
||||
s.spawn(|_| {
|
||||
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
|
||||
chroma_desc.do_(&sample_array)?;
|
||||
Ok(chroma_desc.get_values())
|
||||
});
|
||||
let child_chroma: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> = s.spawn(|_| {
|
||||
let mut chroma_desc = ChromaDesc::new(SAMPLE_RATE, 12);
|
||||
chroma_desc.do_(&sample_array)?;
|
||||
Ok(chroma_desc.get_values())
|
||||
});
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
let child_timbral: thread::ScopedJoinHandle<
|
||||
|
@ -305,8 +325,8 @@ impl Song {
|
|||
Ok(zcr_desc.get_value())
|
||||
});
|
||||
|
||||
let child_loudness: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> = s
|
||||
.spawn(|_| {
|
||||
let child_loudness: thread::ScopedJoinHandle<'_, BlissResult<Vec<f32>>> =
|
||||
s.spawn(|_| {
|
||||
let mut loudness_desc = LoudnessDesc::default();
|
||||
let windows = sample_array.chunks(LoudnessDesc::WINDOW_SIZE);
|
||||
|
||||
|
@ -390,7 +410,6 @@ impl Song {
|
|||
"" => None,
|
||||
a => Some(a.to_string()),
|
||||
};
|
||||
|
||||
};
|
||||
if let Some(album) = format.metadata().get("album") {
|
||||
song.album = match album {
|
||||
|
@ -653,7 +672,7 @@ mod tests {
|
|||
fn test_empty_tags() {
|
||||
let song = Song::decode(Path::new("data/no_tags.flac")).unwrap();
|
||||
assert_eq!(song.artist, None);
|
||||
assert_eq!(song.title, None);
|
||||
assert_eq!(song.title, None);
|
||||
assert_eq!(song.album, None);
|
||||
assert_eq!(song.track_number, None);
|
||||
assert_eq!(song.genre, None);
|
||||
|
@ -804,14 +823,35 @@ mod tests {
|
|||
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))]
|
||||
mod bench {
|
||||
extern crate test;
|
||||
use crate::Song;
|
||||
use test::Bencher;
|
||||
use std::path::Path;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_resample_multi(b: &mut Bencher) {
|
||||
|
|
|
@ -525,8 +525,8 @@ mod bench {
|
|||
use super::*;
|
||||
use crate::Song;
|
||||
use ndarray::Array;
|
||||
use test::Bencher;
|
||||
use std::path::Path;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_convolve(b: &mut Bencher) {
|
||||
|
@ -540,7 +540,9 @@ mod bench {
|
|||
|
||||
#[bench]
|
||||
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(|| {
|
||||
stft(&signal, 2048, 512);
|
||||
|
|
Loading…
Reference in a new issue