diff --git a/CHANGELOG.md b/CHANGELOG.md index 1360528..a9a38d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## bliss 0.3.5 +* Add custom sorting methods for playlist-making. + ## bliss 0.3.4 * Bump ffmpeg's version to avoid building ffmpeg when building bliss. diff --git a/Cargo.toml b/Cargo.toml index 912e1c8..1c89caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bliss-audio" -version = "0.3.4" +version = "0.3.5" authors = ["Polochon-street "] edition = "2018" license = "GPL-3.0-only" diff --git a/src/distance.rs b/src/distance.rs index bd13e09..0db1cd3 100644 --- a/src/distance.rs +++ b/src/distance.rs @@ -7,10 +7,12 @@ //! 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 crate::Library; +use crate::Song; +use crate::NUMBER_FEATURES; use ndarray::{Array, Array1}; +use noisy_float::prelude::*; /// Convenience trait for user-defined distance metrics. pub trait DistanceMetric: Fn(&Array1, &Array1) -> f32 {} @@ -36,10 +38,158 @@ pub fn cosine_distance(a: &Array1, b: &Array1) -> f32 { 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, + 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, 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)] mod test { use super::*; + use crate::Analysis; 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] fn test_euclidean_distance() { diff --git a/src/library.rs b/src/library.rs index e2f8858..c26bc42 100644 --- a/src/library.rs +++ b/src/library.rs @@ -5,10 +5,9 @@ //! MPD](https://github.com/Polochon-street/blissify-rs) could also be useful. #[cfg(doc)] use crate::distance; -use crate::distance::DistanceMetric; +use crate::distance::{closest_to_first_song, DistanceMetric, euclidean_distance}; use crate::{BlissError, BlissResult, Song}; use log::{debug, error, info}; -use noisy_float::prelude::*; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use std::thread; @@ -30,7 +29,8 @@ pub trait Library { /// once. fn get_stored_songs(&self) -> BlissResult>; - /// 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 /// @@ -40,21 +40,22 @@ pub trait Library { /// /// # 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 /// `ret.map(|song| song.path.to_owned()).collect::>()`. + // TODO return an iterator and not a Vec fn playlist_from_song( &self, first_song: Song, playlist_length: usize, ) -> BlissResult> { - let mut songs = self.get_stored_songs()?; - songs.sort_by_cached_key(|song| n32(first_song.distance(&song))); + let playlist = self.playlist_from_song_custom( + first_song, + playlist_length, + euclidean_distance, + closest_to_first_song, + )?; - let playlist = songs - .into_iter() - .take(playlist_length) - .collect::>(); debug!( "Playlist created: {}", playlist @@ -67,7 +68,7 @@ pub trait Library { } /// Return a list of songs that are similar to ``first_song``, using a - /// custom distance metric. + /// custom distance metric and deduplicating indentical songs. /// /// # Arguments /// @@ -98,13 +99,13 @@ pub trait Library { playlist_length: usize, distance: impl DistanceMetric, ) -> BlissResult> { - let mut songs = self.get_stored_songs()?; - songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&song, &distance))); + let playlist = self.playlist_from_song_custom( + first_song, + playlist_length, + distance, + closest_to_first_song, + )?; - let playlist = songs - .into_iter() - .take(playlist_length) - .collect::>(); debug!( "Playlist created: {}", playlist @@ -116,6 +117,44 @@ pub trait Library { 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::>()`. + fn playlist_from_song_custom( + &self, + first_song: Song, + playlist_length: usize, + distance: G, + mut sort: F, + ) -> BlissResult> + where + F: FnMut(&Song, &mut Vec, G), + G: DistanceMetric, + { + let mut songs = self.get_stored_songs()?; + sort(&first_song, &mut songs, distance); + Ok(songs + .into_iter() + .take(playlist_length) + .collect::>()) + } + /// Analyze and store songs in `paths`, using `store_song` and /// `store_error_song` implementations. /// @@ -595,4 +634,49 @@ mod test { .unwrap() ); } + + fn custom_sort(_: &Song, songs: &mut Vec, _: 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() + ); + } }