Add bliss music sorting (sort by song similarity -- songs which are more similar (smaller distance) come first)

This commit is contained in:
NGnius (Graham) 2022-01-25 00:04:25 -05:00
parent 0a6dae930f
commit 41c8c8cbf1
9 changed files with 998 additions and 35 deletions

759
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,10 @@ symphonia = { version = "0.4.0", optional = true, features = [
dirs = { version = "4.0.0" }
regex = { version = "1" }
shellexpand = { version = "2.1", optional = true }
bliss-audio = { version = "0.4", optional = true }
[features]
default = [ "music_library", "ergonomics" ]
default = [ "music_library", "ergonomics", "advanced" ]
music_library = [ "symphonia" ] # song metadata parsing and database auto-population
ergonomics = ["shellexpand"]
ergonomics = ["shellexpand"] # niceties like ~ in pathes
advanced = ["bliss-audio"] # advanced language features like bliss playlist generation

View file

@ -166,6 +166,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
// sorters
.add(crate::lang::vocabulary::sorters::empty_sort())
.add(crate::lang::vocabulary::sorters::field_sort())
.add(crate::lang::vocabulary::sorters::bliss_sort())
// functions and misc
.add(crate::lang::vocabulary::sql_function_factory())
.add(crate::lang::vocabulary::simple_sql_function_factory())

View file

@ -7,16 +7,18 @@ use crate::lang::utility::{assert_name, assert_token_raw, check_name};
use crate::lang::MpsLanguageDictionary;
use crate::lang::{BoxedMpsOpFactory, MpsIteratorItem, MpsOp, PseudoOp};
use crate::lang::{RuntimeError, SyntaxError};
use crate::processing::OpGetter;
use crate::tokens::MpsToken;
use crate::MpsContext;
const SORTER_ITEM_CACHE_SIZE: usize = 8;
pub trait MpsSorter: Clone + Debug + Display {
fn sort(
fn sort<'a>(
&mut self,
iterator: &mut dyn MpsOp,
item_buf: &mut VecDeque<MpsIteratorItem>,
op: &'a mut OpGetter,
) -> Result<(), RuntimeError>;
}
@ -81,11 +83,16 @@ impl<S: MpsSorter + 'static> Iterator for MpsSortStatement<S> {
type Item = MpsIteratorItem;
fn next(&mut self) -> Option<Self::Item> {
let pseudo_self = PseudoOp::from_printable(self);
let real_op = match self.iterable.try_real() {
Ok(op) => op,
Err(e) => return Some(Err(e)),
};
match self.orderer.sort(real_op.as_mut(), &mut self.item_cache) {
match self
.orderer
.sort(real_op.as_mut(), &mut self.item_cache, &mut move || {
pseudo_self.clone()
}) {
Ok(_) => {}
Err(e) => return Some(Err(e)),
}

View file

@ -0,0 +1,236 @@
use std::collections::VecDeque;
#[cfg(feature = "bliss-audio")]
use std::fmt::{Debug, Display, Error, Formatter};
#[cfg(feature = "bliss-audio")]
use std::sync::mpsc::{channel, Receiver};
#[cfg(feature = "bliss-audio")]
use std::collections::HashMap;
#[cfg(feature = "bliss-audio")]
use bliss_audio::Song;
use crate::lang::utility::{assert_name, check_name};
use crate::lang::SyntaxError;
#[cfg(feature = "bliss-audio")]
use crate::lang::{MpsIteratorItem, MpsOp, MpsSorter, MpsTypePrimitive, RuntimeError};
use crate::lang::{MpsLanguageDictionary, MpsSortStatementFactory, MpsSorterFactory};
#[cfg(feature = "bliss-audio")]
use crate::processing::OpGetter;
use crate::tokens::MpsToken;
#[cfg(feature = "bliss-audio")]
const DEFAULT_ORDER: std::cmp::Ordering = std::cmp::Ordering::Greater;
#[cfg(feature = "bliss-audio")]
#[derive(Debug)]
pub struct BlissSorter {
up_to: usize,
float_map: HashMap<String, f32>,
first_song: Option<String>,
rx: Option<Receiver<Result<(String, f32), bliss_audio::BlissError>>>,
errors: Vec<bliss_audio::BlissError>,
}
#[cfg(feature = "bliss-audio")]
impl BlissSorter {
fn get_or_wait(&mut self, path: &str) -> Option<f32> {
if let Some(distance) = self.float_map.get(path) {
Some(*distance)
} else {
// wait on threads until matching result is found
for result in self.rx.as_ref().unwrap() {
match result {
Ok((key, distance)) => {
if path == key {
self.float_map.insert(key, distance);
return Some(distance);
} else {
self.float_map.insert(key, distance);
}
}
Err(e) => {
self.errors.push(e);
return None;
}
}
}
None
}
}
#[inline]
fn compare_songs(
song1: Song,
path_2: String,
) -> Result<(String, f32), bliss_audio::BlissError> {
let song2 = Song::new(&path_2)?;
let distance = song1.distance(&song2);
Ok((path_2, distance))
}
}
#[cfg(feature = "bliss-audio")]
impl std::clone::Clone for BlissSorter {
fn clone(&self) -> Self {
Self {
up_to: self.up_to.clone(),
float_map: self.float_map.clone(),
first_song: self.first_song.clone(),
rx: None,
errors: Vec::new(),
}
}
}
#[cfg(feature = "bliss-audio")]
impl Default for BlissSorter {
fn default() -> Self {
Self {
up_to: usize::MAX,
float_map: HashMap::new(),
first_song: None,
rx: None,
errors: Vec::new(),
}
}
}
#[cfg(feature = "bliss-audio")]
impl MpsSorter for BlissSorter {
fn sort(
&mut self,
iterator: &mut dyn MpsOp,
item_buf: &mut VecDeque<MpsIteratorItem>,
op: &mut OpGetter,
) -> Result<(), RuntimeError> {
let buf_len_old = item_buf.len(); // save buffer length before modifying buffer
if item_buf.len() < self.up_to {
for item in iterator {
item_buf.push_back(item);
if item_buf.len() >= self.up_to {
break;
}
}
}
if buf_len_old != item_buf.len() && !item_buf.is_empty() {
// when buf_len_old == item_buf.len(), iterator was already complete
// no need to sort in that case, since buffer was sorted in last call to sort or buffer never had any items to sort
if self.first_song.is_none() {
let (tx_chann, rx_chann) = channel();
let mut item_paths = Vec::with_capacity(item_buf.len() - 1);
for item in item_buf.iter() {
if let Ok(item) = item {
// build comparison table
if let Some(MpsTypePrimitive::String(path)) = item.field("filename") {
if self.first_song.is_none() {
// find first valid song (ie first item with field "filename")
self.first_song = Some(path.to_owned());
//self.first_song = Some(Song::new(path).map_err(|e| bliss_err(e, op))?);
self.float_map.insert(path.to_owned(), 0.0); // distance to itself should be 0
} else {
item_paths.push(path.to_owned());
}
}
}
}
if let Some(first_song_path) = &self.first_song {
// spawn threads for processing song distances
let path1_clone = first_song_path.to_owned();
std::thread::spawn(move || match Song::new(path1_clone) {
Err(e) => tx_chann.send(Err(e)).unwrap_or(()),
Ok(song1) => {
for path2 in item_paths {
let result_chann = tx_chann.clone();
let song1_clone = song1.clone();
std::thread::spawn(move || {
result_chann
.send(Self::compare_songs(song1_clone, path2))
.unwrap_or(());
});
}
}
});
}
self.rx = Some(rx_chann);
// unordered list returned on first call to this function
// note that only the first item will be used by sorter,
// since the second time this function is called the remaining items are sorted properly
}
} else if self.first_song.is_some() {
// Sort songs on second call to this function
self.first_song = None;
item_buf.make_contiguous().sort_by(|a, b| {
if let Ok(a) = a {
if let Some(MpsTypePrimitive::String(a_path)) = a.field("filename") {
if let Ok(b) = b {
if let Some(MpsTypePrimitive::String(b_path)) = b.field("filename") {
if let Some(float_a) = self.get_or_wait(a_path) {
if let Some(float_b) = self.get_or_wait(b_path) {
println!(
"A:{} distance {}, B:{} distance {}",
a_path, float_a, b_path, float_b
);
return float_a
.partial_cmp(&float_b)
.unwrap_or(DEFAULT_ORDER);
}
}
}
}
}
}
DEFAULT_ORDER
});
}
if self.errors.is_empty() {
Ok(())
} else {
Err(bliss_err(self.errors.pop().unwrap(), op))
}
}
}
#[cfg(feature = "bliss-audio")]
fn bliss_err<D: Display>(error: D, op: &mut OpGetter) -> RuntimeError {
RuntimeError {
line: 0,
op: op(),
msg: format!("Bliss error: {}", error),
}
}
#[cfg(not(feature = "bliss-audio"))]
pub type BlissSorter = crate::lang::vocabulary::sorters::EmptySorter;
#[cfg(feature = "bliss-audio")]
impl Display for BlissSorter {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "distance bliss")
}
}
pub struct BlissSorterFactory;
impl MpsSorterFactory<BlissSorter> for BlissSorterFactory {
fn is_sorter(&self, tokens: &VecDeque<&MpsToken>) -> bool {
tokens.len() == 2 && check_name("distance", &tokens[0]) && check_name("bliss", &tokens[1])
}
fn build_sorter(
&self,
tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<BlissSorter, SyntaxError> {
assert_name("distance", tokens)?;
assert_name("bliss", tokens)?;
Ok(BlissSorter::default())
}
}
pub type BlissSorterStatementFactory = MpsSortStatementFactory<BlissSorter, BlissSorterFactory>;
#[inline(always)]
pub fn bliss_sort() -> BlissSorterStatementFactory {
BlissSorterStatementFactory::new(BlissSorterFactory)
}

View file

@ -4,9 +4,10 @@ use std::fmt::{Debug, Display, Error, Formatter};
use crate::lang::{MpsIteratorItem, MpsLanguageDictionary, MpsOp};
use crate::lang::{MpsSortStatementFactory, MpsSorter, MpsSorterFactory};
use crate::lang::{RuntimeError, SyntaxError};
use crate::processing::OpGetter;
use crate::tokens::MpsToken;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct EmptySorter;
impl MpsSorter for EmptySorter {
@ -14,6 +15,7 @@ impl MpsSorter for EmptySorter {
&mut self,
iterator: &mut dyn MpsOp,
item_buf: &mut VecDeque<MpsIteratorItem>,
_op: &mut OpGetter,
) -> Result<(), RuntimeError> {
if let Some(item) = iterator.next() {
item_buf.push_back(item)

View file

@ -6,6 +6,7 @@ use crate::lang::utility::assert_token;
use crate::lang::{MpsIteratorItem, MpsLanguageDictionary, MpsOp};
use crate::lang::{MpsSortStatementFactory, MpsSorter, MpsSorterFactory};
use crate::lang::{RuntimeError, SyntaxError};
use crate::processing::OpGetter;
use crate::tokens::MpsToken;
#[derive(Debug, Clone)]
@ -20,6 +21,7 @@ impl MpsSorter for FieldSorter {
&mut self,
iterator: &mut dyn MpsOp,
item_buf: &mut VecDeque<MpsIteratorItem>,
_op: &mut OpGetter,
) -> Result<(), RuntimeError> {
let buf_len_old = item_buf.len(); // save buffer length before modifying buffer
if item_buf.len() < self.up_to {
@ -45,7 +47,6 @@ impl MpsSorter for FieldSorter {
}
self.default_order
});
println!("Field-sorted item_buf: {:?}", item_buf);
}
Ok(())
}
@ -80,7 +81,7 @@ impl MpsSorterFactory<FieldSorter> for FieldSorterFactory {
Ok(FieldSorter {
field_name: name,
up_to: usize::MAX,
default_order: Ordering::Equal,
default_order: Ordering::Greater,
})
}
}

View file

@ -1,5 +1,7 @@
mod bliss_sorter;
mod empty_sorter;
mod field_sorter;
pub use bliss_sorter::{bliss_sort, BlissSorter, BlissSorterFactory, BlissSorterStatementFactory};
pub use empty_sorter::{empty_sort, EmptySorter, EmptySorterFactory, EmptySorterStatementFactory};
pub use field_sorter::{field_sort, FieldSorter, FieldSorterFactory, FieldSorterStatementFactory};

View file

@ -358,3 +358,12 @@ fn execute_fieldsort_line() -> Result<(), Box<dyn MpsLanguageError>> {
true,
)
}
#[test]
fn execute_blisssort_line() -> Result<(), Box<dyn MpsLanguageError>> {
execute_single_line(
"files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(distance bliss)",
false,
true,
)
}