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