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