Skip to main content

mecomp_tui/state/
audio.rs

1//! This module contains the implementation of audio state store.
2//! which is updated every tick and used by views to render the audio playback and queue state.
3//!
4//! The audio state store is responsible for maintaining the audio state, and for handling audio related actions.
5
6use std::time::Duration;
7
8use tokio::sync::{
9    broadcast,
10    mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
11};
12
13use mecomp_core::{
14    state::{Percent, StateAudio},
15    udp::StateChange,
16};
17use mecomp_prost::{
18    MusicPlayerClient, PlaybackRepeatRequest, PlaybackSeekRequest, PlaybackSkipRequest,
19    PlaybackVolumeAdjustRequest, QueueRemoveRangeRequest, QueueSetIndexRequest, RecordIdList,
20};
21
22use crate::termination::Interrupted;
23
24use super::action::{AudioAction, PlaybackAction, QueueAction, VolumeAction};
25
26pub const TICK_RATE: Duration = Duration::from_millis(100);
27
28/// The audio state store.
29#[derive(Debug, Clone)]
30#[allow(clippy::module_name_repetitions)]
31pub struct AudioState {
32    state_tx: UnboundedSender<StateAudio>,
33}
34
35impl AudioState {
36    /// create a new audio state store, and return the receiver for listening to state updates.
37    #[must_use]
38    pub fn new() -> (Self, UnboundedReceiver<StateAudio>) {
39        let (state_tx, state_rx) = unbounded_channel::<StateAudio>();
40
41        (Self { state_tx }, state_rx)
42    }
43
44    /// a loop that updates the audio state every tick.
45    ///
46    /// # Errors
47    ///
48    /// Fails if the state cannot be sent
49    pub async fn main_loop(
50        &self,
51        mut daemon: MusicPlayerClient,
52        mut action_rx: UnboundedReceiver<AudioAction>,
53        mut interrupt_rx: broadcast::Receiver<Interrupted>,
54    ) -> anyhow::Result<Interrupted> {
55        let mut state = get_state(&mut daemon).await?;
56        let mut update_needed = false;
57
58        // the initial state once
59        self.state_tx.send(state.clone())?;
60
61        // the ticker
62        let mut ticker = tokio::time::interval(TICK_RATE);
63        ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
64
65        let mut time_last = tokio::time::Instant::now();
66
67        let result = loop {
68            tokio::select! {
69                // Handle the actions coming from the UI
70                // and process them to do async operations
71                Some(action) = action_rx.recv() => {
72                    match action {
73                        AudioAction::Playback(action) => handle_playback(&mut daemon, action).await?,
74                        AudioAction::Queue(action) => handle_queue(&mut daemon, action).await?,
75                        AudioAction::StateChange(state_change) => {
76                            match state_change {
77                                StateChange::Muted => state.muted = true,
78                                StateChange::Unmuted => state.muted = false,
79                                StateChange::VolumeChanged(volume) => state.volume = volume,
80                                StateChange::TrackChanged(_) | StateChange::QueueChanged => {
81                                    // force an update when the track changes, "just in case"
82                                    update_needed = true;
83                                },
84                                StateChange::RepeatModeChanged(repeat_mode) => state.repeat_mode = repeat_mode,
85                                StateChange::Seeked(seek_position) => if let Some(runtime) = &mut state.runtime {
86                                    runtime.seek_percent =
87                                        Percent::new(seek_position.as_secs_f32() / runtime.duration.as_secs_f32() * 100.0);
88                                    runtime.seek_position = seek_position;
89                                },
90                                StateChange::StatusChanged(status) => state.status = status,
91                            }
92                        }
93                    }
94                },
95                // Tick to terminate the select every N milliseconds
96                _ = ticker.tick() => {
97                    if state.paused() {
98                        continue;
99                    }
100                    if let Some(runtime) = &mut state.runtime {
101                        // push the seek position forward by how much time has passed since the last tick
102                        runtime.seek_position+= time_last.elapsed();
103                        runtime.seek_percent =
104                            Percent::new(runtime.seek_position.as_secs_f32() / runtime.duration.as_secs_f32() * 100.0);
105                    }
106                },
107                // Catch and handle interrupt signal to gracefully shutdown
108                Ok(interrupted) = interrupt_rx.recv() => {
109                    break interrupted;
110                }
111            }
112            if update_needed {
113                state = get_state(&mut daemon).await?;
114                update_needed = false;
115            }
116            self.state_tx.send(state.clone())?;
117            time_last = tokio::time::Instant::now();
118        };
119
120        Ok(result)
121    }
122}
123
124/// get the audio state from the daemon.
125async fn get_state(daemon: &mut MusicPlayerClient) -> anyhow::Result<StateAudio> {
126    Ok(daemon
127        .state_audio(())
128        .await?
129        .into_inner()
130        .state
131        .unwrap_or_default()
132        .into())
133}
134
135/// handle a playback action
136async fn handle_playback(
137    daemon: &mut MusicPlayerClient,
138    action: PlaybackAction,
139) -> anyhow::Result<()> {
140    match action {
141        PlaybackAction::Toggle => daemon.playback_toggle(()).await?.into_inner(),
142        PlaybackAction::Next => daemon
143            .playback_skip_forward(PlaybackSkipRequest::new(1))
144            .await?
145            .into_inner(),
146        PlaybackAction::Previous => daemon
147            .playback_skip_backward(PlaybackSkipRequest::new(1))
148            .await?
149            .into_inner(),
150        PlaybackAction::Seek(seek_type, duration) => daemon
151            .playback_seek(PlaybackSeekRequest::new(seek_type, duration))
152            .await?
153            .into_inner(),
154        PlaybackAction::Volume(VolumeAction::Increase(amount)) => daemon
155            .playback_volume_up(PlaybackVolumeAdjustRequest::new(amount))
156            .await?
157            .into_inner(),
158        PlaybackAction::Volume(VolumeAction::Decrease(amount)) => daemon
159            .playback_volume_down(PlaybackVolumeAdjustRequest::new(amount))
160            .await?
161            .into_inner(),
162        PlaybackAction::ToggleMute => daemon.playback_toggle_mute(()).await?.into_inner(),
163    }
164
165    Ok(())
166}
167
168/// handle a queue action
169async fn handle_queue(daemon: &mut MusicPlayerClient, action: QueueAction) -> anyhow::Result<()> {
170    match action {
171        QueueAction::Add(ids) => daemon
172            .queue_add_list(RecordIdList::new(ids))
173            .await?
174            .into_inner(),
175        QueueAction::Remove(index) => daemon
176            .queue_remove_range(QueueRemoveRangeRequest::new(index, index + 1))
177            .await?
178            .into_inner(),
179        QueueAction::SetPosition(index) => daemon
180            .queue_set_index(QueueSetIndexRequest::new(index))
181            .await?
182            .into_inner(),
183        QueueAction::Shuffle => daemon.playback_shuffle(()).await?.into_inner(),
184        QueueAction::Clear => daemon.playback_clear(()).await?.into_inner(),
185        QueueAction::SetRepeatMode(mode) => daemon
186            .playback_repeat(PlaybackRepeatRequest::new(mode))
187            .await?
188            .into_inner(),
189    }
190
191    Ok(())
192}