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