mecomp_core/audio/
commands.rs

1//! This module contains the commands that can be sent to the audio kernel.
2#![allow(clippy::module_name_repetitions)]
3
4use std::{fmt::Display, ops::Range, time::Duration};
5
6use mecomp_storage::db::schemas::song::Song;
7use one_or_many::OneOrMany;
8
9use crate::{
10    format_duration,
11    state::{RepeatMode, SeekType, StateAudio},
12};
13
14/// Commands that can be sent to the audio kernel
15#[derive(Debug)]
16pub enum AudioCommand {
17    Play,
18    Pause,
19    Stop,
20    TogglePlayback,
21    RestartSong,
22    /// only clear the player (i.e. stop playback)
23    ClearPlayer,
24    /// Queue Commands
25    Queue(QueueCommand),
26    /// Stop the audio kernel
27    Exit,
28    /// used to report information about the state of the audio kernel
29    ReportStatus(tokio::sync::oneshot::Sender<StateAudio>),
30    /// volume control commands
31    Volume(VolumeCommand),
32    /// seek commands
33    Seek(SeekType, Duration),
34}
35
36impl PartialEq for AudioCommand {
37    #[inline]
38    fn eq(&self, other: &Self) -> bool {
39        match (self, other) {
40            (Self::Play, Self::Play)
41            | (Self::Pause, Self::Pause)
42            | (Self::TogglePlayback, Self::TogglePlayback)
43            | (Self::ClearPlayer, Self::ClearPlayer)
44            | (Self::RestartSong, Self::RestartSong)
45            | (Self::Exit, Self::Exit)
46            | (Self::Stop, Self::Stop)
47            | (Self::ReportStatus(_), Self::ReportStatus(_)) => true,
48            (Self::Queue(a), Self::Queue(b)) => a == b,
49            (Self::Volume(a), Self::Volume(b)) => a == b,
50            (Self::Seek(a, b), Self::Seek(c, d)) => a == c && b == d,
51            #[cfg(not(tarpaulin_include))]
52            _ => false,
53        }
54    }
55}
56
57impl Display for AudioCommand {
58    #[inline]
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self {
61            Self::Play => write!(f, "Play"),
62            Self::Pause => write!(f, "Pause"),
63            Self::Stop => write!(f, "Stop"),
64            Self::TogglePlayback => write!(f, "Toggle Playback"),
65            Self::RestartSong => write!(f, "Restart Song"),
66            Self::ClearPlayer => write!(f, "Clear Player"),
67            Self::Queue(command) => write!(f, "Queue: {command}"),
68            Self::Exit => write!(f, "Exit"),
69            Self::ReportStatus(_) => write!(f, "Report Status"),
70            Self::Volume(command) => write!(f, "Volume: {command}"),
71            Self::Seek(seek_type, duration) => {
72                write!(
73                    f,
74                    "Seek: {seek_type} {} (HH:MM:SS)",
75                    format_duration(duration)
76                )
77            }
78        }
79    }
80}
81
82/// Queue Commands
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub enum QueueCommand {
85    /// used by the Duration Watcher to signal the player to start the next song,
86    /// this is distinct from calling `SkipForward(1)` in that if the `RepeatMode` is `RepeatMode::One` the song will be restarted
87    PlayNextSong,
88    /// Skip forward in the queue by `n` items
89    SkipForward(usize),
90    /// Skip backward in the queue by `n` items
91    SkipBackward(usize),
92    /// Set the position in the queue to `n`
93    SetPosition(usize),
94    /// Shuffle the queue
95    Shuffle,
96    /// Add a song to the queue
97    AddToQueue(Box<OneOrMany<Song>>),
98    /// Remove a range of items from the queue
99    RemoveRange(Range<usize>),
100    /// Clear the queue
101    Clear,
102    /// Set the repeat mode
103    SetRepeatMode(RepeatMode),
104}
105
106impl Display for QueueCommand {
107    #[inline]
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::SkipForward(n) => write!(f, "Skip Forward by {n}"),
111            Self::SkipBackward(n) => write!(f, "Skip Backward by {n}"),
112            Self::SetPosition(n) => write!(f, "Set Position to {n}"),
113            Self::Shuffle => write!(f, "Shuffle"),
114            Self::AddToQueue(song_box) => match &**song_box {
115                OneOrMany::None => write!(f, "Add nothing"),
116                OneOrMany::One(song) => {
117                    write!(f, "Add \"{}\"", song.title)
118                }
119                OneOrMany::Many(songs) => {
120                    write!(
121                        f,
122                        "Add {:?}",
123                        songs
124                            .iter()
125                            .map(|song| song.title.to_string())
126                            .collect::<Vec<_>>()
127                    )
128                }
129            },
130            Self::RemoveRange(range) => {
131                write!(f, "Remove items {}..{}", range.start, range.end)
132            }
133            Self::Clear => write!(f, "Clear"),
134            Self::SetRepeatMode(mode) => {
135                write!(f, "Set Repeat Mode to {mode}")
136            }
137            Self::PlayNextSong => write!(f, "Play Next Song"),
138        }
139    }
140}
141
142/// Volume commands
143#[derive(Debug, Copy, Clone, PartialEq)]
144pub enum VolumeCommand {
145    Up(f32),
146    Down(f32),
147    Set(f32),
148    Mute,
149    Unmute,
150    ToggleMute,
151}
152
153impl Display for VolumeCommand {
154    #[inline]
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::Up(percent) => write!(f, "+{percent:.0}%", percent = percent * 100.0),
158            Self::Down(percent) => write!(f, "-{percent:.0}%", percent = percent * 100.0),
159            Self::Set(percent) => write!(f, "={percent:.0}%", percent = percent * 100.0),
160            Self::Mute => write!(f, "Mute"),
161            Self::Unmute => write!(f, "Unmute"),
162            Self::ToggleMute => write!(f, "Toggle Mute"),
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use pretty_assertions::assert_str_eq;
170    use rstest::rstest;
171    use std::time::Duration;
172
173    use super::*;
174
175    #[rstest]
176    #[case(AudioCommand::Play, AudioCommand::Play, true)]
177    #[case(AudioCommand::Play, AudioCommand::Pause, false)]
178    #[case(AudioCommand::Pause, AudioCommand::Pause, true)]
179    #[case(AudioCommand::TogglePlayback, AudioCommand::TogglePlayback, true)]
180    #[case(AudioCommand::RestartSong, AudioCommand::RestartSong, true)]
181    #[case(
182        AudioCommand::Queue(QueueCommand::Clear),
183        AudioCommand::Queue(QueueCommand::Clear),
184        true
185    )]
186    #[case(
187        AudioCommand::Queue(QueueCommand::Clear),
188        AudioCommand::Queue(QueueCommand::Shuffle),
189        false
190    )]
191    #[case(
192        AudioCommand::Queue(QueueCommand::SkipForward(1)),
193        AudioCommand::Queue(QueueCommand::SkipForward(1)),
194        true
195    )]
196    #[case(
197        AudioCommand::Queue(QueueCommand::SkipForward(1)),
198        AudioCommand::Queue(QueueCommand::SkipForward(2)),
199        false
200    )]
201    #[case(
202        AudioCommand::Queue(QueueCommand::SkipBackward(1)),
203        AudioCommand::Queue(QueueCommand::SkipBackward(1)),
204        true
205    )]
206    #[case(
207        AudioCommand::Queue(QueueCommand::SkipBackward(1)),
208        AudioCommand::Queue(QueueCommand::SkipBackward(2)),
209        false
210    )]
211    #[case(
212        AudioCommand::Queue(QueueCommand::SetPosition(1)),
213        AudioCommand::Queue(QueueCommand::SetPosition(1)),
214        true
215    )]
216    #[case(
217        AudioCommand::Queue(QueueCommand::SetPosition(1)),
218        AudioCommand::Queue(QueueCommand::SetPosition(2)),
219        false
220    )]
221    #[case(
222        AudioCommand::Queue(QueueCommand::Shuffle),
223        AudioCommand::Queue(QueueCommand::Shuffle),
224        true
225    )]
226    #[case(
227        AudioCommand::Queue(QueueCommand::Shuffle),
228        AudioCommand::Queue(QueueCommand::Clear),
229        false
230    )]
231    #[case(
232        AudioCommand::Volume(VolumeCommand::Up(0.1)),
233        AudioCommand::Volume(VolumeCommand::Up(0.1)),
234        true
235    )]
236    #[case(
237        AudioCommand::Volume(VolumeCommand::Up(0.1)),
238        AudioCommand::Volume(VolumeCommand::Up(0.2)),
239        false
240    )]
241    #[case(
242        AudioCommand::Volume(VolumeCommand::Down(0.1)),
243        AudioCommand::Volume(VolumeCommand::Down(0.1)),
244        true
245    )]
246    #[case(
247        AudioCommand::Volume(VolumeCommand::Down(0.1)),
248        AudioCommand::Volume(VolumeCommand::Down(0.2)),
249        false
250    )]
251    #[case(
252        AudioCommand::Volume(VolumeCommand::Set(0.1)),
253        AudioCommand::Volume(VolumeCommand::Set(0.1)),
254        true
255    )]
256    #[case(
257        AudioCommand::Volume(VolumeCommand::Set(0.1)),
258        AudioCommand::Volume(VolumeCommand::Set(0.2)),
259        false
260    )]
261    #[case(
262        AudioCommand::Volume(VolumeCommand::Mute),
263        AudioCommand::Volume(VolumeCommand::Mute),
264        true
265    )]
266    #[case(
267        AudioCommand::Volume(VolumeCommand::Mute),
268        AudioCommand::Volume(VolumeCommand::Unmute),
269        false
270    )]
271    #[case(
272        AudioCommand::Volume(VolumeCommand::Unmute),
273        AudioCommand::Volume(VolumeCommand::Unmute),
274        true
275    )]
276    #[case(
277        AudioCommand::Volume(VolumeCommand::Unmute),
278        AudioCommand::Volume(VolumeCommand::Mute),
279        false
280    )]
281    #[case(
282        AudioCommand::Volume(VolumeCommand::ToggleMute),
283        AudioCommand::Volume(VolumeCommand::ToggleMute),
284        true
285    )]
286    #[case(
287        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
288        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
289        true
290    )]
291    #[case(
292        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
293        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(20)),
294        false
295    )]
296    #[case(
297        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
298        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
299        true
300    )]
301    #[case(
302        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
303        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(20)),
304        false
305    )]
306    #[case(
307        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
308        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
309        true
310    )]
311    #[case(
312        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
313        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(20)),
314        false
315    )]
316    #[case(
317        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
318        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
319        false
320    )]
321    #[case(
322        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
323        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
324        false
325    )]
326    #[case(
327        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
328        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
329        false
330    )]
331    fn test_audio_command_equality(
332        #[case] lhs: AudioCommand,
333        #[case] rhs: AudioCommand,
334        #[case] expected: bool,
335    ) {
336        let actual = lhs == rhs;
337        assert_eq!(actual, expected);
338        let actual = rhs == lhs;
339        assert_eq!(actual, expected);
340    }
341
342    // dummy song used for display tests, makes the tests more readable
343    fn dummy_song() -> Song {
344        Song {
345            id: Song::generate_id(),
346            title: "Song 1".into(),
347            artist: OneOrMany::None,
348            album_artist: OneOrMany::None,
349            album: "album".into(),
350            genre: OneOrMany::None,
351            runtime: Duration::from_secs(100),
352            track: None,
353            disc: None,
354            release_year: None,
355            extension: "mp3".into(),
356            path: "foo/bar.mp3".into(),
357        }
358    }
359
360    #[rstest]
361    #[case(AudioCommand::Play, "Play")]
362    #[case(AudioCommand::Pause, "Pause")]
363    #[case(AudioCommand::TogglePlayback, "Toggle Playback")]
364    #[case(AudioCommand::ClearPlayer, "Clear Player")]
365    #[case(AudioCommand::RestartSong, "Restart Song")]
366    #[case(AudioCommand::Queue(QueueCommand::Clear), "Queue: Clear")]
367    #[case(AudioCommand::Queue(QueueCommand::Shuffle), "Queue: Shuffle")]
368    #[case(
369        AudioCommand::Queue(QueueCommand::AddToQueue(Box::new(OneOrMany::None))),
370        "Queue: Add nothing"
371    )]
372    #[case(
373        AudioCommand::Queue(QueueCommand::AddToQueue(Box::new(OneOrMany::One(dummy_song())))),
374        "Queue: Add \"Song 1\""
375    )]
376    #[case(
377        AudioCommand::Queue(QueueCommand::AddToQueue(Box::new(OneOrMany::Many(vec![dummy_song()])))),
378        "Queue: Add [\"Song 1\"]"
379    )]
380    #[case(
381        AudioCommand::Queue(QueueCommand::RemoveRange(0..1)),
382        "Queue: Remove items 0..1"
383    )]
384    #[case(
385        AudioCommand::Queue(QueueCommand::SetRepeatMode(RepeatMode::None)),
386        "Queue: Set Repeat Mode to None"
387    )]
388    #[case(
389        AudioCommand::Queue(QueueCommand::SkipForward(1)),
390        "Queue: Skip Forward by 1"
391    )]
392    #[case(
393        AudioCommand::Queue(QueueCommand::SkipBackward(1)),
394        "Queue: Skip Backward by 1"
395    )]
396    #[case(
397        AudioCommand::Queue(QueueCommand::SetPosition(1)),
398        "Queue: Set Position to 1"
399    )]
400    #[case(AudioCommand::Volume(VolumeCommand::Up(0.1)), "Volume: +10%")]
401    #[case(AudioCommand::Volume(VolumeCommand::Down(0.1)), "Volume: -10%")]
402    #[case(AudioCommand::Volume(VolumeCommand::Set(0.1)), "Volume: =10%")]
403    #[case(AudioCommand::Volume(VolumeCommand::Mute), "Volume: Mute")]
404    #[case(AudioCommand::Volume(VolumeCommand::Unmute), "Volume: Unmute")]
405    #[case(AudioCommand::Volume(VolumeCommand::ToggleMute), "Volume: Toggle Mute")]
406    #[case(AudioCommand::Exit, "Exit")]
407    #[case(AudioCommand::ReportStatus(tokio::sync::oneshot::channel().0), "Report Status")]
408    #[case(
409        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(10)),
410        "Seek: Absolute 00:00:10.00 (HH:MM:SS)"
411    )]
412    #[case(
413        AudioCommand::Seek(SeekType::RelativeForwards, Duration::from_secs(10)),
414        "Seek: Forwards 00:00:10.00 (HH:MM:SS)"
415    )]
416    #[case(
417        AudioCommand::Seek(SeekType::RelativeBackwards, Duration::from_secs(10)),
418        "Seek: Backwards 00:00:10.00 (HH:MM:SS)"
419    )]
420    #[case(
421        AudioCommand::Seek(SeekType::Absolute, Duration::from_secs(3600 + 120 + 1)),
422        "Seek: Absolute 01:02:01.00 (HH:MM:SS)"
423    )]
424    fn test_audio_command_display(#[case] command: AudioCommand, #[case] expected: &str) {
425        let actual = command.to_string();
426        assert_str_eq!(actual, expected);
427    }
428}