diff --git a/core/src/audio/mod.rs b/core/src/audio/mod.rs index ac9d17e7..bfa50899 100644 --- a/core/src/audio/mod.rs +++ b/core/src/audio/mod.rs @@ -5,6 +5,7 @@ use std::{ cell::{RefCell, RefMut}, fs::File, io::BufReader, + ops::Range, sync::{ atomic::AtomicBool, mpsc::{Receiver, Sender}, @@ -35,6 +36,18 @@ lazy_static! { Arc::new(AudioKernelSender { tx }) }; } +/// Queue Commands +#[derive(Debug, Clone, PartialEq)] +pub enum QueueCommand { + SkipForward(usize), + SkipBackward(usize), + SetPosition(usize), + Shuffle, + AddToQueue(OneOrMany), + RemoveRange(Range), + Clear, + SetRepeatMode(RepeatMode), +} /// Volume commands #[derive(Debug, Copy, Clone, PartialEq)] @@ -56,12 +69,9 @@ pub enum AudioCommand { RestartSong, /// only clear the player (i.e. stop playback) ClearPlayer, - Clear, - SkipForward(usize), - SkipBackward(usize), - ShuffleQueue, - AddToQueue(OneOrMany), - SetRepeatMode(RepeatMode), + /// Queue Commands + Queue(QueueCommand), + /// Stop the audio kernel Exit, /// used to report information about the state of the audio kernel ReportStatus(tokio::sync::oneshot::Sender), @@ -76,15 +86,10 @@ impl PartialEq for AudioCommand { | (Self::Pause, Self::Pause) | (Self::TogglePlayback, Self::TogglePlayback) | (Self::ClearPlayer, Self::ClearPlayer) - | (Self::Clear, Self::Clear) | (Self::RestartSong, Self::RestartSong) - | (Self::ShuffleQueue, Self::ShuffleQueue) | (Self::Exit, Self::Exit) | (Self::ReportStatus(_), Self::ReportStatus(_)) => true, - (Self::SkipForward(a), Self::SkipForward(b)) - | (Self::SkipBackward(a), Self::SkipBackward(b)) => a == b, - (Self::AddToQueue(a), Self::AddToQueue(b)) => a == b, - (Self::SetRepeatMode(a), Self::SetRepeatMode(b)) => a == b, + (Self::Queue(a), Self::Queue(b)) => a == b, (Self::Volume(a), Self::Volume(b)) => a == b, _ => false, } @@ -181,20 +186,7 @@ impl AudioKernel { AudioCommand::TogglePlayback => self.toggle_playback(), AudioCommand::RestartSong => self.restart_song(), AudioCommand::ClearPlayer => self.clear_player(), - AudioCommand::Clear => self.clear(), - AudioCommand::SkipForward(n) => self.skip_forward(n), - AudioCommand::SkipBackward(n) => self.skip_backward(n), - AudioCommand::ShuffleQueue => self.queue.borrow_mut().shuffle(), - AudioCommand::AddToQueue(OneOrMany::None) => {} - AudioCommand::AddToQueue(OneOrMany::One(song)) => { - self.add_song_to_queue(song); - } - AudioCommand::AddToQueue(OneOrMany::Many(songs)) => { - self.add_songs_to_queue(songs); - } - AudioCommand::SetRepeatMode(mode) => { - self.queue.borrow_mut().set_repeat_mode(mode); - } + AudioCommand::Queue(command) => self.queue_control(command), AudioCommand::Exit => break, AudioCommand::ReportStatus(tx) => { let current_song = self.queue.borrow().current_song().cloned(); @@ -256,6 +248,21 @@ impl AudioKernel { self.player.clear(); } + fn queue_control(&self, command: QueueCommand) { + match command { + QueueCommand::Clear => self.clear(), + QueueCommand::SkipForward(n) => self.skip_forward(n), + QueueCommand::SkipBackward(n) => self.skip_backward(n), + QueueCommand::SetPosition(n) => self.set_position(n), + QueueCommand::Shuffle => self.queue.borrow_mut().shuffle(), + QueueCommand::AddToQueue(OneOrMany::None) => {} + QueueCommand::AddToQueue(OneOrMany::One(song)) => self.add_song_to_queue(song), + QueueCommand::AddToQueue(OneOrMany::Many(songs)) => self.add_songs_to_queue(songs), + QueueCommand::RemoveRange(range) => self.remove_range_from_queue(range), + QueueCommand::SetRepeatMode(mode) => self.queue.borrow_mut().set_repeat_mode(mode), + } + } + fn clear(&self) { self.clear_player(); self.queue.borrow_mut().clear(); @@ -301,6 +308,24 @@ impl AudioKernel { } } + fn set_position(&self, n: usize) { + let paused = self.player.is_paused(); + self.clear_player(); + + let mut binding = self.queue(); + binding.set_current_index(n); + let next_song = binding.current_song(); + + if let Some(song) = next_song { + if let Err(e) = self.append_song_to_player(song) { + error!("Failed to append song to player: {e}"); + } + if !paused { + self.play(); + } + } + } + fn add_song_to_queue(&self, song: Song) { { let mut binding = self.queue(); @@ -314,7 +339,7 @@ impl AudioKernel { current_index.map_or_else(|| self.get_next_song(), |_| self.get_current_song()) { if let Err(e) = self.append_song_to_player(&song) { - error!("Failed to append song to player: {}", e); + error!("Failed to append song to player: {e}"); } self.play(); } @@ -334,13 +359,38 @@ impl AudioKernel { current_index.map_or_else(|| self.get_next_song(), |_| self.get_current_song()) { if let Err(e) = self.append_song_to_player(&song) { - error!("Failed to append song to player: {}", e); + error!("Failed to append song to player: {e}"); } self.play(); } } } + // TODO: test that the player stops playing when the current song is removed, + fn remove_range_from_queue(&self, range: Range) { + let paused = if self.player.is_paused() { + true + } else if let Some(current_index) = self.queue.borrow().current_index() { + // still true if the current song is to be removed + range.contains(¤t_index) + } else { + false + }; + self.clear_player(); + + let mut binding = self.queue(); + binding.remove_range(range); + + if let Some(song) = binding.current_song() { + if let Err(e) = self.append_song_to_player(song) { + error!("Failed to append song to player: {e}"); + } + if !paused { + self.play(); + } + } + } + fn get_current_song(&self) -> Option { let binding = self.queue.borrow(); binding.current_song().cloned() @@ -525,27 +575,33 @@ mod tests { assert_eq!(state.queue_position, None); assert!(state.paused); - sender.send(AudioCommand::AddToQueue(OneOrMany::One(song.clone()))); - sender.send(AudioCommand::AddToQueue(OneOrMany::One(song.clone()))); - sender.send(AudioCommand::AddToQueue(OneOrMany::One(song.clone()))); + sender.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::One(song.clone()), + ))); + sender.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::One(song.clone()), + ))); + sender.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::One(song.clone()), + ))); // songs were added to an empty queue, so the first song should start playing let state = get_state(sender.clone()); assert_eq!(state.queue_position, Some(0)); assert!(!state.paused); - sender.send(AudioCommand::SkipForward(1)); + sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1))); // the second song should start playing let state = get_state(sender.clone()); assert_eq!(state.queue_position, Some(1)); assert!(!state.paused); - sender.send(AudioCommand::SkipForward(1)); + sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1))); // the third song should start playing let state = get_state(sender.clone()); assert_eq!(state.queue_position, Some(2)); assert!(!state.paused); - sender.send(AudioCommand::SkipForward(1)); + sender.send(AudioCommand::Queue(QueueCommand::SkipForward(1))); // we were at the end of the queue and tried to skip forward, so the player should be paused and the queue position should be None let state = get_state(sender.clone()); assert_eq!(state.queue_position, None); diff --git a/core/src/audio/queue.rs b/core/src/audio/queue.rs index 2f135438..cf1bc59f 100644 --- a/core/src/audio/queue.rs +++ b/core/src/audio/queue.rs @@ -179,6 +179,38 @@ impl Queue { pub fn queued_songs(&self) -> Box<[Song]> { self.songs.clone().into_boxed_slice() } + + /// Sets the current index, clamped to the nearest valid index. + pub fn set_current_index(&mut self, index: usize) { + if self.songs.is_empty() { + self.current_index = None; + } else { + self.current_index = Some(index.min(self.songs.len().saturating_sub(1))); + } + } + + /// Removes a range of songs from the queue. + /// If the current index is within the range, it will be set to the next valid index (or the + /// previous valid index if the range included the end of the queue). + pub fn remove_range(&mut self, range: std::ops::Range) { + if range.is_empty() || self.is_empty() { + return; + } + let current_index = self.current_index.unwrap_or_default(); + let range_end = range.end.min(self.songs.len()); + let range_start = range.start.min(range_end); + + self.songs.drain(range_start..range_end); + + if current_index >= range_start && current_index < range_end { + self.current_index = Some(range_start + 1); + } else if current_index >= range_end { + self.current_index = Some(current_index - (range_end - range_start)); + } + if self.current_index.unwrap_or_default() >= self.songs.len() || self.is_empty() { + self.current_index = None; + } + } } #[cfg(test)] @@ -186,7 +218,8 @@ mod tests { use super::*; use crate::state::RepeatMode; use crate::test_utils::{ - arb_song_case, arb_vec, bar_sc, baz_sc, create_song, foo_sc, init, SongCase, TIMEOUT, + arb_song_case, arb_vec, arb_vec_and_range_and_index, bar_sc, baz_sc, create_song, foo_sc, + init, RangeEndMode, RangeIndexMode, RangeStartMode, SongCase, TIMEOUT, }; use mecomp_storage::db::init_test_database; @@ -390,4 +423,94 @@ mod tests { queue.set_repeat_mode(repeat_mode); assert_eq!(queue.repeat_mode, repeat_mode); } + + #[rstest] + #[case::within_range( arb_vec(&arb_song_case(), 5..=10 )(), 3 )] + #[case::at_start( arb_vec(&arb_song_case(), 5..=10 )(), 0 )] + #[case::at_end( arb_vec(&arb_song_case(), 10..=10 )(), 9 )] + #[case::empty( arb_vec(&arb_song_case(),0..=0)(), 0)] + #[case::out_of_range( arb_vec(&arb_song_case(), 5..=10 )(), 15 )] + #[tokio::test] + async fn test_set_current_index( + #[case] songs: Vec, + #[case] index: usize, + ) -> anyhow::Result<()> { + let db = init_test_database().await?; + init().await?; + let mut queue = Queue::new(); + let len = songs.len(); + for sc in songs { + queue.add_song(create_song(&db, sc).await?); + } + + queue.set_current_index(index); + + if len == 0 { + assert_eq!(queue.current_index, None); + } else if index >= len { + assert_eq!(queue.current_index, Some(len - 1)); + } else { + assert_eq!(queue.current_index, Some(index.min(len - 1))); + } + + Ok(()) + } + + #[rstest] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::InRange )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::BeforeRange )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::AfterRangeInBounds )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::OutOfBounds )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::InBounds )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::OutOfBounds,RangeEndMode::Standard, RangeIndexMode::InRange )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 0..=0,RangeStartMode::Zero,RangeEndMode::Start, RangeIndexMode::InBounds )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10, RangeStartMode::Standard, RangeEndMode::Start, RangeIndexMode::InBounds)() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::InBounds )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::InRange )() )] + #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::BeforeRange )() )] + #[tokio::test] + async fn test_remove_range( + #[case] params: (Vec, std::ops::Range, Option), + ) -> anyhow::Result<()> { + let (songs, range, index) = params; + let len = songs.len(); + let db = init_test_database().await?; + init().await?; + let mut queue = Queue::new(); + for sc in songs { + queue.add_song(create_song(&db, sc).await?); + } + + if let Some(index) = index { + queue.set_current_index(index); + } + + let unmodified_songs = queue.clone(); + + queue.remove_range(range.clone()); + + let start = range.start; + let end = range.end.min(len); + + // our tests fall into 4 categories: + // 1. nothing is removed (start==end or start>=len) + // 2. everything is removed (start==0 and end>=len) + // 3. only some songs are removed(end>start>0) + + if start >= len || start == end { + assert_eq!(queue.len(), len); + } else if start == 0 && end >= len { + assert_eq!(queue.len(), 0); + assert_eq!(queue.current_index, None); + } else { + assert_eq!(queue.len(), len - (end.min(len) - start)); + for i in 0..start { + assert_eq!(queue.get(i), unmodified_songs.get(i)); + } + for i in end..len { + assert_eq!(queue.get(i - (end - start)), unmodified_songs.get(i)); + } + } + Ok(()) + } } diff --git a/core/src/test_utils.rs b/core/src/test_utils.rs index 6a104e74..fcb8d943 100644 --- a/core/src/test_utils.rs +++ b/core/src/test_utils.rs @@ -1,4 +1,7 @@ -use std::{ops::RangeInclusive, time::Duration}; +use std::{ + ops::{Range, RangeInclusive}, + time::Duration, +}; use audiotags; use lazy_static::lazy_static; @@ -142,22 +145,94 @@ pub fn arb_song_case() -> impl Fn() -> SongCase { } } +pub enum IndexMode { + InBounds, + OutOfBounds, +} + pub fn arb_vec_and_index( item_strategy: &impl Fn() -> T, range: RangeInclusive, + index_mode: IndexMode, ) -> impl Fn() -> (Vec, usize) + '_ where T: Clone + std::fmt::Debug + Sized, { move || { let vec = arb_vec(item_strategy, range.clone())(); - let index = (0..vec.len()) - .choose(&mut rand::thread_rng()) - .unwrap_or_default(); + let index = match index_mode { + IndexMode::InBounds => 0..vec.len(), + IndexMode::OutOfBounds => vec.len()..(vec.len() + vec.len() / 2 + 1), + } + .choose(&mut rand::thread_rng()) + .unwrap_or_default(); (vec, index) } } +pub enum RangeStartMode { + Standard, + Zero, + OutOfBounds, +} + +pub enum RangeEndMode { + Start, + Standard, + OutOfBounds, +} + +pub enum RangeIndexMode { + InBounds, + InRange, + AfterRangeInBounds, + OutOfBounds, + BeforeRange, +} + +// Returns a tuple of a Vec of T and a Range +// where the start is a random index in the Vec +// and the end is a random index in the Vec that is greater than or equal to the start +pub fn arb_vec_and_range_and_index( + item_strategy: &impl Fn() -> T, + range: RangeInclusive, + range_start_mode: RangeStartMode, + range_end_mode: RangeEndMode, + index_mode: RangeIndexMode, +) -> impl Fn() -> (Vec, Range, Option) + '_ +where + T: Clone + std::fmt::Debug + Sized, +{ + move || { + let vec = arb_vec(item_strategy, range.clone())(); + let start = match range_start_mode { + RangeStartMode::Standard => 0..vec.len(), + RangeStartMode::OutOfBounds => vec.len()..(vec.len() + vec.len() / 2 + 1), + RangeStartMode::Zero => 0..1, + } + .choose(&mut rand::thread_rng()) + .unwrap_or_default(); + let end = match range_end_mode { + RangeEndMode::Standard => start..vec.len(), + RangeEndMode::OutOfBounds => vec.len()..(vec.len() + vec.len() / 2 + 1).max(start), + RangeEndMode::Start => start..(start + 1), + } + .choose(&mut rand::thread_rng()) + .unwrap_or_default(); + + let index = match index_mode { + RangeIndexMode::InBounds => 0..vec.len(), + RangeIndexMode::InRange => start..end, + RangeIndexMode::AfterRangeInBounds => end..vec.len(), + RangeIndexMode::OutOfBounds => vec.len()..(vec.len() + vec.len() / 2 + 1), + RangeIndexMode::BeforeRange => 0..start, + } + .choose(&mut rand::thread_rng()); + + (vec, start..end, index) + } +} + pub fn arb_vec( item_strategy: &impl Fn() -> T, range: RangeInclusive, diff --git a/daemon/src/controller.rs b/daemon/src/controller.rs index 60edb063..a1bb2fb4 100644 --- a/daemon/src/controller.rs +++ b/daemon/src/controller.rs @@ -8,7 +8,7 @@ use rand::seq::SliceRandom; use tap::TapFallible; //-------------------------------------------------------------------------------- MECOMP libraries use mecomp_core::{ - audio::{AudioCommand, VolumeCommand, AUDIO_KERNEL}, + audio::{AudioCommand, QueueCommand, VolumeCommand, AUDIO_KERNEL}, errors::SerializableLibraryError, rpc::MusicPlayer, search::SearchResult, @@ -489,7 +489,7 @@ impl MusicPlayer for MusicPlayerServer { info!("Playing next song"); tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::SkipForward(1)); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::SkipForward(1))); } .in_current_span(), ) @@ -515,7 +515,7 @@ impl MusicPlayer for MusicPlayerServer { info!("Skipping forward by {} songs", amount); tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::SkipForward(amount)); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::SkipForward(amount))); } .in_current_span(), ) @@ -528,7 +528,7 @@ impl MusicPlayer for MusicPlayerServer { info!("Going back by {} songs", amount); tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::SkipBackward(amount)); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::SkipBackward(amount))); } .in_current_span(), ) @@ -555,7 +555,7 @@ impl MusicPlayer for MusicPlayerServer { info!("Clearing queue and stopping playback"); tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::Clear); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::Clear)); } .in_current_span(), ) @@ -574,7 +574,7 @@ impl MusicPlayer for MusicPlayerServer { info!("Setting repeat mode to: {}", mode); tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::SetRepeatMode(mode)); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::SetRepeatMode(mode))); } .in_current_span(), ) @@ -587,7 +587,7 @@ impl MusicPlayer for MusicPlayerServer { info!("Shuffling queue"); tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::ShuffleQueue); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::Shuffle)); } .in_current_span(), ) @@ -689,7 +689,9 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(OneOrMany::One(song))); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::One(song), + ))); } .in_current_span(), ) @@ -712,7 +714,7 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(songs.into())); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue(songs.into()))); } .in_current_span(), ) @@ -735,7 +737,7 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(songs.into())); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue(songs.into()))); } .in_current_span(), ) @@ -758,7 +760,7 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(songs.into())); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue(songs.into()))); } .in_current_span(), ) @@ -781,7 +783,7 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(songs.into())); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue(songs.into()))); } .in_current_span(), ) @@ -804,7 +806,9 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(OneOrMany::One(song))); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::One(song), + ))); } .in_current_span(), ) @@ -835,7 +839,9 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(OneOrMany::Many(songs))); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::Many(songs), + ))); } .in_current_span(), ) @@ -866,7 +872,9 @@ impl MusicPlayer for MusicPlayerServer { tokio::spawn( async move { - AUDIO_KERNEL.send(AudioCommand::AddToQueue(OneOrMany::Many(songs))); + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::AddToQueue( + OneOrMany::Many(songs), + ))); } .in_current_span(), ) @@ -880,14 +888,30 @@ impl MusicPlayer for MusicPlayerServer { #[instrument] async fn queue_set_index(self, context: Context, index: usize) { info!("Setting queue index to: {}", index); - todo!() + + tokio::spawn( + async move { + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::SetPosition(index))); + } + .in_current_span(), + ) + .await + .unwrap(); } /// remove a range of songs from the queue. /// if the range is out of bounds, it will be clamped to the nearest valid range. #[instrument] async fn queue_remove_range(self, context: Context, range: Range) { info!("Removing queue range: {:?}", range); - todo!() + + tokio::spawn( + async move { + AUDIO_KERNEL.send(AudioCommand::Queue(QueueCommand::RemoveRange(range))); + } + .in_current_span(), + ) + .await + .unwrap(); } /// Returns brief information about the users playlists.