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::Song;
7
8#[derive(Clone, Debug, Deserialize, Serialize)]
9pub struct Queue {
10    songs: Vec<Song>,
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: Song) {
35        self.songs.push(song);
36    }
37
38    #[instrument]
39    pub fn add_songs(&mut self, songs: Vec<Song>) {
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<&Song> {
73        self.current_index.and_then(|index| self.songs.get(index))
74    }
75
76    #[instrument]
77    pub fn next_song(&mut self) -> Option<&Song> {
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<&Song> {
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<&Song> {
125        self.skip_backward(1)
126    }
127
128    #[instrument]
129    pub fn skip_backward(&mut self, n: usize) -> Option<&Song> {
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<&Song> {
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<[Song]> {
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        queue.add_song(song.clone());
284        assert_eq!(queue.len(), 1);
285        assert_eq!(queue.songs[0], song);
286        assert_eq!(queue.current_song(), None);
287
288        Ok(())
289    }
290
291    #[tokio::test]
292    async fn test_add_songs() {
293        init();
294        let db = init_test_database().await.unwrap();
295
296        let songs = vec![
297            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
298                .await
299                .unwrap(),
300            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
301                .await
302                .unwrap(),
303            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
304                .await
305                .unwrap(),
306        ];
307        let mut queue = Queue::new();
308        queue.add_songs(songs.clone());
309        assert_eq!(queue.len(), 3);
310        assert_eq!(queue.queued_songs(), songs.into_boxed_slice());
311        assert_eq!(queue.current_song(), None);
312    }
313
314    #[rstest]
315    #[case::index_oob(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()], 1, 4, Some(1))]
316    #[case::index_before_current(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()], 2, 1, Some(1))]
317    #[case::index_after_current(vec![arb_song_case()(),arb_song_case()(),arb_song_case()()], 1, 2, Some(1))]
318    #[case::index_at_current(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()],  1, 1, Some(0))]
319    #[case::index_at_current_zero(vec![arb_song_case()(), arb_song_case()(), arb_song_case()()],  0, 0, Some(0))]
320    #[case::remove_only_song(vec![arb_song_case()()], 0, 0, None )]
321    #[tokio::test]
322    async fn test_remove_song(
323        #[case] songs: Vec<SongCase>,
324        #[case] current_index_before: usize,
325        #[case] index_to_remove: usize,
326        #[case] expected_current_index_after: Option<usize>,
327    ) {
328        init();
329        let db = init_test_database().await.unwrap();
330        let mut queue = Queue::new();
331
332        // add songs and set index
333        for sc in songs {
334            queue.add_song(
335                create_song_with_overrides(&db, sc, SongChangeSet::default())
336                    .await
337                    .unwrap(),
338            );
339        }
340        queue.set_current_index(current_index_before);
341
342        // remove specified song
343        queue.remove_song(index_to_remove);
344
345        // assert current index is as expected
346        assert_eq!(queue.current_index(), expected_current_index_after);
347    }
348
349    #[rstest]
350    #[case::one_song(arb_vec_and_index( &arb_song_case(), 1..=1, IndexMode::InBounds)())]
351    #[case::many_songs(arb_vec_and_index( &arb_song_case(), 2..=10, IndexMode::InBounds)())]
352    #[case::many_songs_guaranteed_nonzero_index((arb_vec( &arb_song_case(), 2..=10)(), 1))]
353    #[tokio::test]
354    async fn test_shuffle(#[case] params: (Vec<SongCase>, usize)) {
355        init();
356        let (songs, index) = params;
357        let db = init_test_database().await.unwrap();
358        let mut queue = Queue::default();
359
360        // add songs to queue and set index
361        for sc in songs {
362            queue.add_song(
363                create_song_with_overrides(&db, sc, SongChangeSet::default())
364                    .await
365                    .unwrap(),
366            );
367        }
368        queue.set_current_index(index);
369
370        let current_song = queue.current_song().cloned();
371
372        // shuffle queue
373        queue.shuffle();
374
375        // assert that the current song doesn't change and that current index is 0
376        assert_eq!(queue.current_song().cloned(), current_song);
377        assert_eq!(queue.current_index(), Some(0));
378    }
379
380    #[tokio::test]
381    async fn test_next_previous_basic() -> anyhow::Result<()> {
382        init();
383        let db = init_test_database().await.unwrap();
384
385        let mut queue = Queue::new();
386        let song1 =
387            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default()).await?;
388        let song2 =
389            create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default()).await?;
390        queue.add_song(song1.clone());
391        queue.add_song(song2.clone());
392        assert_eq!(queue.next_song(), Some(&song1));
393        assert_eq!(queue.next_song(), Some(&song2));
394        assert_eq!(queue.previous_song(), Some(&song1));
395        assert_eq!(queue.previous_song(), None);
396
397        queue.clear();
398        assert_eq!(queue.next_song(), None);
399        assert_eq!(queue.previous_song(), None);
400
401        Ok(())
402    }
403
404    #[tokio::test]
405    async fn test_next_song_with_rp_one() {
406        init();
407        let db = init_test_database().await.unwrap();
408
409        let mut queue = Queue::new();
410        queue.set_repeat_mode(RepeatMode::One);
411        let song1 = create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
412            .await
413            .unwrap();
414        let song2 = create_song_with_overrides(&db, arb_song_case()(), SongChangeSet::default())
415            .await
416            .unwrap();
417        queue.add_song(song1.clone());
418        queue.add_song(song2.clone());
419
420        assert_eq!(queue.current_song(), None);
421        assert_eq!(queue.next_song(), Some(&song1));
422        assert_eq!(queue.current_song(), Some(&song1));
423        assert_eq!(queue.next_song(), Some(&song1));
424        queue.skip_forward(1);
425        assert_eq!(queue.current_song(), Some(&song2));
426        assert_eq!(queue.next_song(), Some(&song2));
427        queue.skip_forward(1);
428        assert_eq!(queue.current_song(), None);
429        assert_eq!(queue.next_song(), None);
430    }
431
432    #[template]
433    #[rstest]
434    #[case::more_than_len( arb_vec(&arb_song_case(), 4..=5 )(), 7 )]
435    #[case::way_more_than_len( arb_vec(&arb_song_case(), 3..=5 )(), 11 )]
436    #[case::skip_len( arb_vec(&arb_song_case(), 5..=5 )(), 5 )]
437    #[case::skip_len_twice( arb_vec(&arb_song_case(), 5..=5 )(), 10 )]
438    #[case::less_than_len( arb_vec(&arb_song_case(), 4..=5 )(), 3 )]
439    #[case::skip_one( arb_vec(&arb_song_case(), 2..=5 )(), 1 )]
440    #[timeout(std::time::Duration::from_secs(30))]
441    pub fn skip_song_test_template(#[case] songs: Vec<SongCase>, #[case] skip: usize) {}
442
443    #[apply(skip_song_test_template)]
444    #[tokio::test]
445    async fn test_skip_song_rp_none(songs: Vec<SongCase>, skip: usize) -> anyhow::Result<()> {
446        init();
447        let db = init_test_database().await.unwrap();
448
449        let mut queue = Queue::new();
450        let len = songs.len();
451        for sc in songs {
452            queue.add_song(create_song_with_overrides(&db, sc, SongChangeSet::default()).await?);
453        }
454        queue.set_repeat_mode(RepeatMode::None);
455
456        queue.skip_forward(skip);
457
458        if skip <= len {
459            assert_eq!(
460                queue.current_song(),
461                queue.get(skip - 1),
462                "len: {len}, skip: {skip}, current_index: {current_index}",
463                current_index = queue.current_index.unwrap_or_default()
464            );
465        } else {
466            assert_eq!(
467                queue.current_song(),
468                None,
469                "len: {len}, skip: {skip}, current_index: {current_index}",
470                current_index = queue.current_index.unwrap_or_default()
471            );
472        }
473
474        Ok(())
475    }
476
477    #[apply(skip_song_test_template)]
478    #[tokio::test]
479    async fn test_skip_song_rp_one(songs: Vec<SongCase>, skip: usize) -> anyhow::Result<()> {
480        init();
481        let db = init_test_database().await.unwrap();
482
483        let mut queue = Queue::new();
484        let len = songs.len();
485        for sc in songs {
486            queue.add_song(create_song_with_overrides(&db, sc, SongChangeSet::default()).await?);
487        }
488        queue.set_repeat_mode(RepeatMode::One);
489
490        queue.skip_forward(skip);
491
492        if skip <= len {
493            // if we haven't reached the end of the queue
494            assert_eq!(
495                queue.current_song(),
496                queue.get(skip - 1),
497                "len: {len}, skip: {skip}, current_index: {current_index}",
498                current_index = queue.current_index.unwrap_or_default()
499            );
500        } else {
501            // if we reached the end of the queue
502            assert_eq!(
503                queue.current_song(),
504                None,
505                "len: {len}, skip: {skip}, current_index: {current_index}",
506                current_index = queue.current_index.unwrap_or_default()
507            );
508        }
509
510        Ok(())
511    }
512
513    #[apply(skip_song_test_template)]
514    #[tokio::test]
515    async fn test_next_song_rp_all(songs: Vec<SongCase>, skip: usize) -> anyhow::Result<()> {
516        init();
517        let db = init_test_database().await.unwrap();
518
519        let mut queue = Queue::new();
520        let len = songs.len();
521        for sc in songs {
522            queue.add_song(create_song_with_overrides(&db, sc, SongChangeSet::default()).await?);
523        }
524        queue.set_repeat_mode(RepeatMode::All);
525
526        queue.skip_forward(skip);
527
528        assert_eq!(
529            queue.current_song(),
530            queue.get((skip - 1) % len),
531            "len: {len}, skip: {skip}, current_index: {current_index}",
532            current_index = queue.current_index.unwrap_or_default()
533        );
534
535        Ok(())
536    }
537
538    #[rstest]
539    #[case(RepeatMode::None)]
540    #[case(RepeatMode::One)]
541    #[case(RepeatMode::All)]
542    #[test]
543    fn test_set_repeat_mode(#[case] repeat_mode: RepeatMode) {
544        let mut queue = Queue::new();
545        queue.set_repeat_mode(repeat_mode);
546        assert_eq!(queue.repeat_mode, repeat_mode);
547    }
548
549    #[rstest]
550    #[case::within_range( arb_vec(&arb_song_case(), 5..=10 )(), 3 )]
551    #[case::at_start( arb_vec(&arb_song_case(), 5..=10 )(), 0 )]
552    #[case::at_end( arb_vec(&arb_song_case(), 10..=10 )(), 9 )]
553    #[case::empty( arb_vec(&arb_song_case(),0..=0)(), 0)]
554    #[case::out_of_range( arb_vec(&arb_song_case(), 5..=10 )(), 15 )]
555    #[tokio::test]
556    async fn test_set_current_index(
557        #[case] songs: Vec<SongCase>,
558        #[case] index: usize,
559    ) -> anyhow::Result<()> {
560        init();
561        let db = init_test_database().await?;
562
563        let mut queue = Queue::new();
564        let len = songs.len();
565        for sc in songs {
566            queue.add_song(create_song_with_overrides(&db, sc, SongChangeSet::default()).await?);
567        }
568
569        queue.set_current_index(index);
570
571        if len == 0 {
572            assert_eq!(queue.current_index, None);
573        } else if index >= len {
574            assert_eq!(queue.current_index, Some(len - 1));
575        } else {
576            assert_eq!(queue.current_index, Some(index.min(len - 1)));
577        }
578
579        Ok(())
580    }
581
582    #[rstest]
583    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::InRange )() )]
584    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::BeforeRange )() )]
585    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::AfterRangeInBounds )() )]
586    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::OutOfBounds )() )]
587    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::Standard, RangeIndexMode::InBounds )() )]
588    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::OutOfBounds,RangeEndMode::Standard, RangeIndexMode::InRange )() )]
589    #[case( arb_vec_and_range_and_index(&arb_song_case(), 0..=0,RangeStartMode::Zero,RangeEndMode::Start, RangeIndexMode::InBounds )() )]
590    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10, RangeStartMode::Standard, RangeEndMode::Start, RangeIndexMode::InBounds)() )]
591    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::InBounds )() )]
592    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::InRange )() )]
593    #[case( arb_vec_and_range_and_index(&arb_song_case(), 5..=10,RangeStartMode::Standard,RangeEndMode::OutOfBounds, RangeIndexMode::BeforeRange )() )]
594    #[tokio::test]
595    async fn test_remove_range(
596        #[case] params: (Vec<SongCase>, std::ops::Range<usize>, Option<usize>),
597    ) -> anyhow::Result<()> {
598        init();
599        let (songs, range, index) = params;
600        let len = songs.len();
601        let db = init_test_database().await?;
602
603        let mut queue = Queue::new();
604        for sc in songs {
605            queue.add_song(create_song_with_overrides(&db, sc, SongChangeSet::default()).await?);
606        }
607
608        if let Some(index) = index {
609            queue.set_current_index(index);
610        }
611
612        let unmodified_songs = queue.clone();
613
614        queue.remove_range(range.clone());
615
616        let start = range.start;
617        let end = range.end.min(len);
618
619        // our tests fall into 4 categories:
620        // 1. nothing is removed (start==end or start>=len)
621        // 2. everything is removed (start==0 and end>=len)
622        // 3. only some songs are removed(end>start>0)
623
624        if start >= len || start == end {
625            assert_eq!(queue.len(), len);
626        } else if start == 0 && end >= len {
627            assert_eq!(queue.len(), 0);
628            assert_eq!(queue.current_index, None);
629        } else {
630            assert_eq!(queue.len(), len - (end.min(len) - start));
631            for i in 0..start {
632                assert_eq!(queue.get(i), unmodified_songs.get(i));
633            }
634            for i in end..len {
635                assert_eq!(queue.get(i - (end - start)), unmodified_songs.get(i));
636            }
637        }
638        Ok(())
639    }
640}