mecomp_core/audio/
queue.rs

1use rand::{prelude::SliceRandom, thread_rng};
2use serde::{Deserialize, Serialize};
3use tracing::instrument;
4
5use crate::state::RepeatMode;
6use mecomp_storage::db::schemas::song::SongBrief;
7
8#[derive(Clone, Debug, Deserialize, Serialize)]
9pub struct Queue {
10    songs: Vec<SongBrief>,
11    current_index: Option<usize>,
12    repeat_mode: RepeatMode,
13}
14
15impl Default for Queue {
16    #[inline]
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl Queue {
23    #[must_use]
24    #[inline]
25    pub const fn new() -> Self {
26        Self {
27            songs: Vec::new(),
28            current_index: None,
29            repeat_mode: RepeatMode::None,
30        }
31    }
32
33    #[instrument]
34    pub fn add_song(&mut self, song: SongBrief) {
35        self.songs.push(song);
36    }
37
38    #[instrument]
39    pub fn add_songs(&mut self, songs: Vec<SongBrief>) {
40        self.songs.extend(songs);
41    }
42
43    #[instrument]
44    pub fn remove_song(&mut self, index: usize) {
45        if index >= self.len() {
46            return;
47        }
48
49        match self.current_index {
50            Some(current_index)
51                if current_index > index || (current_index == index && current_index > 0) =>
52            {
53                self.current_index = Some(current_index - 1);
54            }
55            Some(_) if self.len() <= 1 => {
56                self.current_index = None;
57            }
58            _ => {}
59        }
60
61        self.songs.remove(index);
62    }
63
64    #[instrument]
65    pub fn clear(&mut self) {
66        self.songs.clear();
67        self.current_index = None;
68    }
69
70    #[must_use]
71    #[instrument]
72    pub fn current_song(&self) -> Option<&SongBrief> {
73        self.current_index.and_then(|index| self.songs.get(index))
74    }
75
76    #[instrument]
77    pub fn next_song(&mut self) -> Option<&SongBrief> {
78        if self.repeat_mode == RepeatMode::One && self.current_index.is_some() {
79            self.current_song()
80        } else {
81            self.skip_forward(1)
82        }
83    }
84
85    /// Skip forward n songs in the queue.
86    ///
87    /// progresses the current index by n, following the repeat mode rules.
88    #[instrument]
89    pub fn skip_forward(&mut self, n: usize) -> Option<&SongBrief> {
90        match self.current_index {
91            Some(current_index) if current_index + n < self.songs.len() => {
92                self.current_index = Some(current_index + n);
93                self.current_index.and_then(|index| self.songs.get(index))
94            }
95            Some(current_index) => {
96                match self.repeat_mode {
97                    RepeatMode::None | RepeatMode::One => {
98                        // if we reach this point, then skipping would put us at the end of the queue,
99                        // so let's just stop playback
100                        self.current_index = None;
101                        self.songs.clear();
102                        None
103                    }
104                    RepeatMode::All => {
105                        // if we reach this point, then skipping would put us past the end of the queue,
106                        // so let's emulate looping over the songs as many times as needed, then skipping the remaining songs
107                        self.current_index = Some((current_index + n) % self.songs.len());
108                        self.current_index.and_then(|index| self.songs.get(index))
109                    }
110                }
111            }
112            None => {
113                if self.songs.is_empty() || n == 0 {
114                    return None;
115                }
116
117                self.current_index = Some(0);
118                self.skip_forward(n - 1)
119            }
120        }
121    }
122
123    #[instrument]
124    pub fn previous_song(&mut self) -> Option<&SongBrief> {
125        self.skip_backward(1)
126    }
127
128    #[instrument]
129    pub fn skip_backward(&mut self, n: usize) -> Option<&SongBrief> {
130        match self.current_index {
131            Some(current_index) if current_index >= n => {
132                self.current_index = Some(current_index - n);
133                self.current_index.and_then(|index| self.songs.get(index))
134            }
135            _ => {
136                self.current_index = None;
137                None
138            }
139        }
140    }
141
142    #[instrument]
143    pub fn set_repeat_mode(&mut self, repeat_mode: RepeatMode) {
144        self.repeat_mode = repeat_mode;
145    }
146
147    #[must_use]
148    #[inline]
149    pub const fn get_repeat_mode(&self) -> RepeatMode {
150        self.repeat_mode
151    }
152
153    #[instrument]
154    pub fn shuffle(&mut self) {
155        // swap current song to first
156        match self.current_index {
157            Some(current_index) if current_index != 0 && !self.is_empty() => {
158                self.songs.swap(0, current_index);
159                self.current_index = Some(0);
160            }
161            _ => {}
162        }
163        if self.len() <= 1 {
164            return;
165        }
166        // shuffle the slice from [1..]
167        self.songs[1..].shuffle(&mut thread_rng());
168    }
169
170    #[must_use]
171    #[instrument]
172    pub fn get(&self, index: usize) -> Option<&SongBrief> {
173        self.songs.get(index)
174    }
175
176    #[must_use]
177    #[instrument]
178    pub fn len(&self) -> usize {
179        self.songs.len()
180    }
181
182    #[must_use]
183    #[instrument]
184    pub fn is_empty(&self) -> bool {
185        self.songs.is_empty()
186    }
187
188    #[must_use]
189    #[inline]
190    pub const fn current_index(&self) -> Option<usize> {
191        self.current_index
192    }
193
194    #[must_use]
195    #[instrument]
196    pub fn queued_songs(&self) -> Box<[SongBrief]> {
197        self.songs.clone().into_boxed_slice()
198    }
199
200    /// Sets the current index, clamped to the nearest valid index.
201    #[instrument]
202    pub fn set_current_index(&mut self, index: usize) {
203        if self.songs.is_empty() {
204            self.current_index = None;
205        } else {
206            self.current_index = Some(index.min(self.songs.len() - 1));
207        }
208    }
209
210    /// Removes a range of songs from the queue.
211    /// If the current index is within the range, it will be set to the next valid index (or the
212    /// previous valid index if the range included the end of the queue).
213    #[instrument]
214    pub fn remove_range(&mut self, range: std::ops::Range<usize>) {
215        if range.is_empty() || self.is_empty() {
216            return;
217        }
218        let current_index = self.current_index.unwrap_or_default();
219        let range_end = range.end.min(self.songs.len());
220        let range_start = range.start.min(range_end);
221
222        self.songs.drain(range_start..range_end);
223
224        if current_index >= range_start && current_index < range_end {
225            // current index is within the range
226            self.current_index = Some(range_start);
227        } else if current_index >= range_end {
228            // current index is after the range
229            self.current_index = Some(current_index - (range_end - range_start));
230        }
231
232        // if the current index was put out of bounds, set it to None
233        if self.current_index.unwrap_or_default() >= self.songs.len() || self.is_empty() {
234            self.current_index = None;
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use crate::state::RepeatMode;
243    use crate::test_utils::init;
244
245    use mecomp_storage::db::schemas::song::SongChangeSet;
246    use mecomp_storage::test_utils::{
247        IndexMode, RangeEndMode, RangeIndexMode, RangeStartMode, SongCase, arb_song_case, arb_vec,
248        arb_vec_and_index, arb_vec_and_range_and_index, create_song_with_overrides,
249        init_test_database,
250    };
251
252    use pretty_assertions::assert_eq;
253    use rstest::*;
254    use rstest_reuse;
255    use rstest_reuse::{apply, template};
256
257    #[test]
258    fn test_new_queue() {
259        let mut queue = Queue::default();
260        assert_eq!(queue.len(), 0);
261        assert_eq!(queue.current_index(), None);
262        assert_eq!(queue.get_repeat_mode(), RepeatMode::None);
263
264        assert_eq!(queue.current_song(), None);
265        assert_eq!(queue.next_song(), None);
266        assert_eq!(queue.current_index, None);
267        assert_eq!(queue.previous_song(), None);
268        assert_eq!(queue.current_index, None);
269    }
270
271    #[rstest]
272    #[case(arb_song_case()())]
273    #[case(arb_song_case()())]
274    #[case(arb_song_case()())]
275    #[tokio::test]
276    async fn test_add_song(#[case] song: SongCase) -> anyhow::Result<()> {
277        init();
278
279        let db = init_test_database().await.unwrap();
280
281        let mut queue = Queue::new();
282        let song = create_song_with_overrides(&db, song, SongChangeSet::default()).await?;
283        let song: SongBrief = song.into();
284        queue.add_song(song.clone());
285        assert_eq!(queue.len(), 1);
286        assert_eq!(queue.songs[0], song);
287        assert_eq!(queue.current_song(), None);
288
289        Ok(())
290    }
291
292    #[tokio::test]
293    async fn test_add_songs() {
294        init();
295        let db = init_test_database().await.unwrap();
296
297        let songs = vec![
298            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
299                .await
300                .unwrap()
301                .into(),
302            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
303                .await
304                .unwrap()
305                .into(),
306            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
307                .await
308                .unwrap()
309                .into(),
310        ];
311        let mut queue = Queue::new();
312        queue.add_songs(songs.clone());
313        assert_eq!(queue.len(), 3);
314        assert_eq!(queue.queued_songs(), songs.into_boxed_slice());
315        assert_eq!(queue.current_song(), None);
316    }
317
318    #[rstest]
319    #[case::index_oob(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()], 1, 4, Some(1))]
320    #[case::index_before_current(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()], 2, 1, Some(1))]
321    #[case::index_after_current(vec![arb_song_case()(),arb_song_case()(),arb_song_case()()], 1, 2, Some(1))]
322    #[case::index_at_current(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()],  1, 1, Some(0))]
323    #[case::index_at_current_zero(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()],  0, 0, Some(0))]
324    #[case::remove_only_song(vec![arb_song_case()()], 0, 0, None )]
325    #[tokio::test]
326    async fn test_remove_song(
327        #[case] songs: Vec<SongCase>,
328        #[case] current_index_before: usize,
329        #[case] index_to_remove: usize,
330        #[case] expected_current_index_after: Option<usize>,
331    ) {
332        init();
333        let db = init_test_database().await.unwrap();
334        let mut queue = Queue::new();
335
336        // add songs and set index
337        for sc in songs {
338            queue.add_song(
339                create_song_with_overrides(&db, sc, SongChangeSet::default())
340                    .await
341                    .unwrap()
342                    .into(),
343            );
344        }
345        queue.set_current_index(current_index_before);
346
347        // remove specified song
348        queue.remove_song(index_to_remove);
349
350        // assert current index is as expected
351        assert_eq!(queue.current_index(), expected_current_index_after);
352    }
353
354    #[rstest]
355    #[case::one_song(arb_vec_and_index( &arb_song_case(), 1..=1, IndexMode::InBounds)())]
356    #[case::many_songs(arb_vec_and_index( &arb_song_case(), 2..=10, IndexMode::InBounds)())]
357    #[case::many_songs_guaranteed_nonzero_index((arb_vec( &arb_song_case(), 2..=10)(), 1))]
358    #[tokio::test]
359    async fn test_shuffle(#[case] params: (Vec<SongCase>, usize)) {
360        init();
361        let (songs, index) = params;
362        let db = init_test_database().await.unwrap();
363        let mut queue = Queue::default();
364
365        // add songs to queue and set index
366        for sc in songs {
367            queue.add_song(
368                create_song_with_overrides(&db, sc, SongChangeSet::default())
369                    .await
370                    .unwrap()
371                    .into(),
372            );
373        }
374        queue.set_current_index(index);
375
376        let current_song = queue.current_song().cloned();
377
378        // shuffle queue
379        queue.shuffle();
380
381        // assert that the current song doesn't change and that current index is 0
382        assert_eq!(queue.current_song().cloned(), current_song);
383        assert_eq!(queue.current_index(), Some(0));
384    }
385
386    #[tokio::test]
387    async fn test_next_previous_basic() -> anyhow::Result<()> {
388        init();
389        let db = init_test_database().await.unwrap();
390
391        let mut queue = Queue::new();
392        let song1: SongBrief =
393            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
394                .await?
395                .into();
396        let song2: SongBrief =
397            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
398                .await?
399                .into();
400        queue.add_song(song1.clone());
401        queue.add_song(song2.clone());
402        assert_eq!(queue.next_song(), Some(&song1));
403        assert_eq!(queue.next_song(), Some(&song2));
404        assert_eq!(queue.previous_song(), Some(&song1));
405        assert_eq!(queue.previous_song(), None);
406
407        queue.clear();
408        assert_eq!(queue.next_song(), None);
409        assert_eq!(queue.previous_song(), None);
410
411        Ok(())
412    }
413
414    #[tokio::test]
415    async fn test_next_song_with_rp_one() {
416        init();
417        let db = init_test_database().await.unwrap();
418
419        let mut queue = Queue::new();
420        queue.set_repeat_mode(RepeatMode::One);
421        let song1: SongBrief =
422            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
423                .await
424                .unwrap()
425                .into();
426        let song2: SongBrief =
427            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
428                .await
429                .unwrap()
430                .into();
431        queue.add_song(song1.clone());
432        queue.add_song(song2.clone());
433
434        assert_eq!(queue.current_song(), None);
435        assert_eq!(queue.next_song(), Some(&song1));
436        assert_eq!(queue.current_song(), Some(&song1));
437        assert_eq!(queue.next_song(), Some(&song1));
438        queue.skip_forward(1);
439        assert_eq!(queue.current_song(), Some(&song2));
440        assert_eq!(queue.next_song(), Some(&song2));
441        queue.skip_forward(1);
442        assert_eq!(queue.current_song(), None);
443        assert_eq!(queue.next_song(), None);
444    }
445
446    #[template]
447    #[rstest]
448    #[case::more_than_len( arb_vec(&arb_song_case(), 4..=5 )(), 7 )]
449    #[case::way_more_than_len( arb_vec(&arb_song_case(), 3..=5 )(), 11 )]
450    #[case::skip_len( arb_vec(&arb_song_case(), 5..=5 )(), 5 )]
451    #[case::skip_len_twice( arb_vec(&arb_song_case(), 5..=5 )(), 10 )]
452    #[case::less_than_len( arb_vec(&arb_song_case(), 4..=5 )(), 3 )]
453    #[case::skip_one( arb_vec(&arb_song_case(), 2..=5 )(), 1 )]
454    #[timeout(std::time::Duration::from_secs(30))]
455    pub fn skip_song_test_template(#[case] songs: Vec<SongCase>, #[case] skip: usize) {}
456
457    #[apply(skip_song_test_template)]
458    #[tokio::test]
459    async fn test_skip_song_rp_none(songs: Vec<SongCase>, skip: usize) -> anyhow::Result<()> {
460        init();
461        let db = init_test_database().await.unwrap();
462
463        let mut queue = Queue::new();
464        let len = songs.len();
465        for sc in songs {
466            queue.add_song(
467                create_song_with_overrides(&db, sc, SongChangeSet::default())
468                    .await?
469                    .into(),
470            );
471        }
472        queue.set_repeat_mode(RepeatMode::None);
473
474        queue.skip_forward(skip);
475
476        if skip <= len {
477            assert_eq!(
478                queue.current_song(),
479                queue.get(skip - 1),
480                "len: {len}, skip: {skip}, current_index: {current_index}",
481                current_index = queue.current_index.unwrap_or_default()
482            );
483        } else {
484            assert_eq!(
485                queue.current_song(),
486                None,
487                "len: {len}, skip: {skip}, current_index: {current_index}",
488                current_index = queue.current_index.unwrap_or_default()
489            );
490        }
491
492        Ok(())
493    }
494
495    #[apply(skip_song_test_template)]
496    #[tokio::test]
497    async fn test_skip_song_rp_one(songs: Vec<SongCase>, skip: usize) -> anyhow::Result<()> {
498        init();
499        let db = init_test_database().await.unwrap();
500
501        let mut queue = Queue::new();
502        let len = songs.len();
503        for sc in songs {
504            queue.add_song(
505                create_song_with_overrides(&db, sc, SongChangeSet::default())
506                    .await?
507                    .into(),
508            );
509        }
510        queue.set_repeat_mode(RepeatMode::One);
511
512        queue.skip_forward(skip);
513
514        if skip <= len {
515            // if we haven't reached the end of the queue
516            assert_eq!(
517                queue.current_song(),
518                queue.get(skip - 1),
519                "len: {len}, skip: {skip}, current_index: {current_index}",
520                current_index = queue.current_index.unwrap_or_default()
521            );
522        } else {
523            // if we reached the end of the queue
524            assert_eq!(
525                queue.current_song(),
526                None,
527                "len: {len}, skip: {skip}, current_index: {current_index}",
528                current_index = queue.current_index.unwrap_or_default()
529            );
530        }
531
532        Ok(())
533    }
534
535    #[apply(skip_song_test_template)]
536    #[tokio::test]
537    async fn test_next_song_rp_all(songs: Vec<SongCase>, skip: usize) -> anyhow::Result<()> {
538        init();
539        let db = init_test_database().await.unwrap();
540
541        let mut queue = Queue::new();
542        let len = songs.len();
543        for sc in songs {
544            queue.add_song(
545                create_song_with_overrides(&db, sc, SongChangeSet::default())
546                    .await?
547                    .into(),
548            );
549        }
550        queue.set_repeat_mode(RepeatMode::All);
551
552        queue.skip_forward(skip);
553
554        assert_eq!(
555            queue.current_song(),
556            queue.get((skip - 1) % len),
557            "len: {len}, skip: {skip}, current_index: {current_index}",
558            current_index = queue.current_index.unwrap_or_default()
559        );
560
561        Ok(())
562    }
563
564    #[rstest]
565    #[case(RepeatMode::None)]
566    #[case(RepeatMode::One)]
567    #[case(RepeatMode::All)]
568    #[test]
569    fn test_set_repeat_mode(#[case] repeat_mode: RepeatMode) {
570        let mut queue = Queue::new();
571        queue.set_repeat_mode(repeat_mode);
572        assert_eq!(queue.repeat_mode, repeat_mode);
573    }
574
575    #[rstest]
576    #[case::within_range( arb_vec(&arb_song_case(), 5..=10 )(), 3 )]
577    #[case::at_start( arb_vec(&arb_song_case(), 5..=10 )(), 0 )]
578    #[case::at_end( arb_vec(&arb_song_case(), 10..=10 )(), 9 )]
579    #[case::empty( arb_vec(&arb_song_case(),0..=0)(), 0)]
580    #[case::out_of_range( arb_vec(&arb_song_case(), 5..=10 )(), 15 )]
581    #[tokio::test]
582    async fn test_set_current_index(
583        #[case] songs: Vec<SongCase>,
584        #[case] index: usize,
585    ) -> anyhow::Result<()> {
586        init();
587        let db = init_test_database().await?;
588
589        let mut queue = Queue::new();
590        let len = songs.len();
591        for sc in songs {
592            queue.add_song(
593                create_song_with_overrides(&db, sc, SongChangeSet::default())
594                    .await?
595                    .into(),
596            );
597        }
598
599        queue.set_current_index(index);
600
601        if len == 0 {
602            assert_eq!(queue.current_index, None);
603        } else if index >= len {
604            assert_eq!(queue.current_index, Some(len - 1));
605        } else {
606            assert_eq!(queue.current_index, Some(index.min(len - 1)));
607        }
608
609        Ok(())
610    }
611
612    #[rstest]
613    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::InRange )() )]
614    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::BeforeRange )() )]
615    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::AfterRangeInBounds )() )]
616    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::OutOfBounds )() )]
617    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::InBounds )() )]
618    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::OutOfBounds,RangeEndMode::Standard, RangeIndexMode::InRange )() )]
619    #[case( arb_vec_and_range_and_index(&arb_song_case(), 0..=0,RangeStartMode::Zero,RangeEndMode::Start, RangeIndexMode::InBounds )() )]
620    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10, RangeStartMode::Standard, RangeEndMode::Start, RangeIndexMode::InBounds)() )]
621    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::InBounds )() )]
622    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::InRange )() )]
623    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::BeforeRange )() )]
624    #[tokio::test]
625    async fn test_remove_range(
626        #[case] params: (Vec<SongCase>, std::ops::Range<usize>, Option<usize>),
627    ) -> anyhow::Result<()> {
628        init();
629        let (songs, range, index) = params;
630        let len = songs.len();
631        let db = init_test_database().await?;
632
633        let mut queue = Queue::new();
634        for sc in songs {
635            queue.add_song(
636                create_song_with_overrides(&db, sc, SongChangeSet::default())
637                    .await?
638                    .into(),
639            );
640        }
641
642        if let Some(index) = index {
643            queue.set_current_index(index);
644        }
645
646        let unmodified_songs = queue.clone();
647
648        queue.remove_range(range.clone());
649
650        let start = range.start;
651        let end = range.end.min(len);
652
653        // our tests fall into 4 categories:
654        // 1. nothing is removed (start==end or start>=len)
655        // 2. everything is removed (start==0 and end>=len)
656        // 3. only some songs are removed(end>start>0)
657
658        if start >= len || start == end {
659            assert_eq!(queue.len(), len);
660        } else if start == 0 && end >= len {
661            assert_eq!(queue.len(), 0);
662            assert_eq!(queue.current_index, None);
663        } else {
664            assert_eq!(queue.len(), len - (end.min(len) - start));
665            for i in 0..start {
666                assert_eq!(queue.get(i), unmodified_songs.get(i));
667            }
668            for i in end..len {
669                assert_eq!(queue.get(i - (end - start)), unmodified_songs.get(i));
670            }
671        }
672        Ok(())
673    }
674}