Add custom sorting for playlists

This commit is contained in:
Polochon-street 2021-07-22 18:07:40 +02:00
parent 82d229346b
commit e9e63f961c
4 changed files with 257 additions and 20 deletions

View file

@ -1,5 +1,8 @@
# Changelog # Changelog
## bliss 0.3.5
* Add custom sorting methods for playlist-making.
## bliss 0.3.4 ## bliss 0.3.4
* Bump ffmpeg's version to avoid building ffmpeg when building bliss. * Bump ffmpeg's version to avoid building ffmpeg when building bliss.

View file

@ -1,6 +1,6 @@
[package] [package]
name = "bliss-audio" name = "bliss-audio"
version = "0.3.4" version = "0.3.5"
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"

View file

@ -7,10 +7,12 @@
//! They will yield different styles of playlists, so don't hesitate to //! They will yield different styles of playlists, so don't hesitate to
//! experiment with them if the default (euclidean distance for now) doesn't //! experiment with them if the default (euclidean distance for now) doesn't
//! suit you. //! suit you.
use crate::NUMBER_FEATURES;
#[cfg(doc)] #[cfg(doc)]
use crate::{Library, Song}; use crate::Library;
use crate::Song;
use crate::NUMBER_FEATURES;
use ndarray::{Array, Array1}; use ndarray::{Array, Array1};
use noisy_float::prelude::*;
/// Convenience trait for user-defined distance metrics. /// Convenience trait for user-defined distance metrics.
pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {} pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
@ -36,10 +38,158 @@ pub fn cosine_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
1. - similarity 1. - similarity
} }
/// Sort `songs` in place by putting songs close to `first_song` first
/// using the `distance` metric. Deduplicate identical songs.
pub fn closest_to_first_song(
first_song: &Song,
songs: &mut Vec<Song>,
distance: impl DistanceMetric,
) {
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance)));
songs.dedup_by_key(|song| n32(first_song.custom_distance(song, &distance)));
}
/// Sort `songs` in place using the `distance` metric and ordering by
/// the smallest distance between each song. Deduplicate identical songs.
///
/// If the generated playlist is `[song1, song2, song3, song4]`, it means
/// song2 is closest to song1, song3 is closest to song2, and song4 is closest
/// to song3.
pub fn song_to_song(first_song: &Song, songs: &mut Vec<Song>, distance: impl DistanceMetric) {
let mut new_songs = vec![first_song.to_owned()];
let mut song = first_song.to_owned();
loop {
if songs.is_empty() {
break;
}
songs
.retain(|s| n32(song.custom_distance(s, &distance)) != 0.);
songs.sort_by_key(|s| n32(song.custom_distance(s, &distance)));
song = songs.remove(0);
new_songs.push(song.to_owned());
}
*songs = new_songs;
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Analysis;
use ndarray::arr1; use ndarray::arr1;
use std::path::Path;
#[test]
fn test_song_to_song() {
let first_song = Song {
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let first_song_dupe = Song {
path: Path::new("path-to-dupe").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let second_song = Song {
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
]),
..Default::default()
};
let third_song = Song {
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
]),
..Default::default()
};
let fourth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let mut songs = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
];
song_to_song(&first_song, &mut songs, euclidean_distance);
assert_eq!(
songs,
vec![first_song, second_song, third_song, fourth_song],
);
}
#[test]
fn test_sort_closest_to_first_song() {
let first_song = Song {
path: Path::new("path-to-first").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let first_song_dupe = Song {
path: Path::new("path-to-dupe").to_path_buf(),
analysis: Analysis::new([
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
]),
..Default::default()
};
let second_song = Song {
path: Path::new("path-to-second").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
]),
..Default::default()
};
let third_song = Song {
path: Path::new("path-to-third").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
]),
..Default::default()
};
let fourth_song = Song {
path: Path::new("path-to-fourth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let fifth_song = Song {
path: Path::new("path-to-fifth").to_path_buf(),
analysis: Analysis::new([
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
]),
..Default::default()
};
let mut songs = vec![
first_song.to_owned(),
first_song_dupe.to_owned(),
second_song.to_owned(),
third_song.to_owned(),
fourth_song.to_owned(),
fifth_song.to_owned(),
];
closest_to_first_song(&first_song, &mut songs, euclidean_distance);
assert_eq!(
songs,
vec![first_song, second_song, fourth_song, third_song],
);
}
#[test] #[test]
fn test_euclidean_distance() { fn test_euclidean_distance() {

View file

@ -5,10 +5,9 @@
//! MPD](https://github.com/Polochon-street/blissify-rs) could also be useful. //! MPD](https://github.com/Polochon-street/blissify-rs) could also be useful.
#[cfg(doc)] #[cfg(doc)]
use crate::distance; use crate::distance;
use crate::distance::DistanceMetric; use crate::distance::{closest_to_first_song, DistanceMetric, euclidean_distance};
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 std::sync::mpsc; use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender}; use std::sync::mpsc::{Receiver, Sender};
use std::thread; use std::thread;
@ -30,7 +29,8 @@ pub trait Library {
/// once. /// once.
fn get_stored_songs(&self) -> BlissResult<Vec<Song>>; fn get_stored_songs(&self) -> BlissResult<Vec<Song>>;
/// Return a list of songs that are similar to ``first_song``. /// Return a list of `playlist_length` songs that are similar
/// to ``first_song``, deduplicating identical songs.
/// ///
/// # Arguments /// # Arguments
/// ///
@ -40,21 +40,22 @@ pub trait Library {
/// ///
/// # Returns /// # Returns
/// ///
/// A vector of `playlist_length` Songs, including `first_song`, that you /// A vector of `playlist_length` songs, including `first_song`, that you
/// most likely want to plug in your audio player by using something like /// most likely want to plug in your audio player by using something like
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`. /// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
// TODO return an iterator and not a Vec
fn playlist_from_song( fn playlist_from_song(
&self, &self,
first_song: Song, first_song: Song,
playlist_length: usize, playlist_length: usize,
) -> BlissResult<Vec<Song>> { ) -> BlissResult<Vec<Song>> {
let mut songs = self.get_stored_songs()?; let playlist = self.playlist_from_song_custom(
songs.sort_by_cached_key(|song| n32(first_song.distance(&song))); first_song,
playlist_length,
euclidean_distance,
closest_to_first_song,
)?;
let playlist = songs
.into_iter()
.take(playlist_length)
.collect::<Vec<Song>>();
debug!( debug!(
"Playlist created: {}", "Playlist created: {}",
playlist playlist
@ -67,7 +68,7 @@ pub trait Library {
} }
/// Return a list of songs that are similar to ``first_song``, using a /// Return a list of songs that are similar to ``first_song``, using a
/// custom distance metric. /// custom distance metric and deduplicating indentical songs.
/// ///
/// # Arguments /// # Arguments
/// ///
@ -98,13 +99,13 @@ pub trait Library {
playlist_length: usize, playlist_length: usize,
distance: impl DistanceMetric, distance: impl DistanceMetric,
) -> BlissResult<Vec<Song>> { ) -> BlissResult<Vec<Song>> {
let mut songs = self.get_stored_songs()?; let playlist = self.playlist_from_song_custom(
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&song, &distance))); first_song,
playlist_length,
distance,
closest_to_first_song,
)?;
let playlist = songs
.into_iter()
.take(playlist_length)
.collect::<Vec<Song>>();
debug!( debug!(
"Playlist created: {}", "Playlist created: {}",
playlist playlist
@ -116,6 +117,44 @@ pub trait Library {
Ok(playlist) Ok(playlist)
} }
/// Return a playlist of songs, starting with `first_song`, sorted using
/// the custom `sort` function, and the 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.
/// * `sort` - a user-supplied sorting function that uses the `distance`
/// metric, either taken from the [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>>()`.
fn playlist_from_song_custom<F, G>(
&self,
first_song: Song,
playlist_length: usize,
distance: G,
mut sort: F,
) -> BlissResult<Vec<Song>>
where
F: FnMut(&Song, &mut Vec<Song>, G),
G: DistanceMetric,
{
let mut songs = self.get_stored_songs()?;
sort(&first_song, &mut songs, distance);
Ok(songs
.into_iter()
.take(playlist_length)
.collect::<Vec<Song>>())
}
/// Analyze and store songs in `paths`, using `store_song` and /// Analyze and store songs in `paths`, using `store_song` and
/// `store_error_song` implementations. /// `store_error_song` implementations.
/// ///
@ -595,4 +634,49 @@ mod test {
.unwrap() .unwrap()
); );
} }
fn custom_sort(_: &Song, songs: &mut Vec<Song>, _: impl DistanceMetric) {
songs.sort_by_key(|song| song.path.to_owned());
}
#[test]
fn test_playlist_from_song_custom() {
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, second_song],
test_library
.playlist_from_song_custom(first_song, 3, custom_distance, custom_sort)
.unwrap()
);
}
} }