diff --git a/audio-player/src/player.rs b/audio-player/src/player.rs index 94ef8b2..46acbef 100644 --- a/audio-player/src/player.rs +++ b/audio-player/src/player.rs @@ -124,6 +124,15 @@ impl Player { Ok(rx.recv_async().await?) } + /// Toggles mute; resolves to the new muted state. + pub async fn toggle_mute(&self) -> Result { + let (tx, rx) = flume::bounded(1); + self.tx_engine + .send_async(PlayerEngineCommand::ToggleMute(tx)) + .await?; + Ok(rx.recv_async().await?) + } + pub async fn pause(&self) -> Result<()> { let (tx, rx) = flume::bounded(1); self.tx_engine diff --git a/audio-player/src/player_engine.rs b/audio-player/src/player_engine.rs index 835d3fb..bf03620 100644 --- a/audio-player/src/player_engine.rs +++ b/audio-player/src/player_engine.rs @@ -47,6 +47,7 @@ pub enum PlayerEngineCommand { GetElapsed(Sender>), SeekTo(Duration, Sender>), GetVolume(Sender), + ToggleMute(Sender), GetPaused(Sender>), /// End of stream for the source started by the given generation. /// Stale generations are ignored so an old track finishing can never @@ -100,6 +101,10 @@ pub struct PlayerEngine { /// visualizer (architecture/spectrum.md). Handed out via /// [`Self::spectrum_tap`] so the server's FFT task can read it. spectrum: Arc, + /// Whether output is muted (sink volume zeroed). + muted: bool, + /// Volume to restore on unmute. + pre_mute_volume: f32, } impl PlayerEngine { @@ -140,6 +145,8 @@ impl PlayerEngine { _owned_runtime: owned_runtime, http, spectrum, + muted: false, + pre_mute_volume: 1.0, }) } @@ -174,6 +181,7 @@ impl PlayerEngine { send_reply(tx, self.set_volume(volume)); } PlayerEngineCommand::GetVolume(tx) => send_reply(tx, self.volume()), + PlayerEngineCommand::ToggleMute(tx) => send_reply(tx, self.toggle_mute()), PlayerEngineCommand::GetPaused(tx) => send_reply(tx, self.is_paused()), PlayerEngineCommand::Eos(generation) => self.handle_eos(generation), } @@ -386,13 +394,37 @@ impl PlayerEngine { Ok(self.sink.get_pos()) } + /// The user's intended volume — the level playback would resume at, + /// which while muted is the remembered pre-mute level rather than the + /// silenced sink volume. pub fn volume(&self) -> f32 { + if self.muted { + self.pre_mute_volume + } else { + self.sink.volume() + } + } + + /// Sets the volume and unmutes: reaching for the volume is an intent + /// to hear something. + pub fn set_volume(&mut self, volume: f32) -> f32 { + self.muted = false; + self.sink.set_volume(volume.clamp(0.0, 1.1)); self.sink.volume() } - pub fn set_volume(&mut self, volume: f32) -> f32 { - self.sink.set_volume(volume.clamp(0.0, 1.1)); - self.sink.volume() + /// Toggles mute by zeroing the sink volume and remembering the level + /// to restore. Returns the new muted state. + pub fn toggle_mute(&mut self) -> bool { + if self.muted { + self.sink.set_volume(self.pre_mute_volume); + self.muted = false; + } else { + self.pre_mute_volume = self.sink.volume(); + self.sink.set_volume(0.0); + self.muted = true; + } + self.muted } fn handle_eos(&mut self, generation: u64) { diff --git a/cbd-tui/src/app/now_playing.rs b/cbd-tui/src/app/now_playing.rs index 2ee1740..48bfd5b 100644 --- a/cbd-tui/src/app/now_playing.rs +++ b/cbd-tui/src/app/now_playing.rs @@ -55,6 +55,8 @@ pub struct NowPlaying { spectrum: Vec, /// Whether to draw the spectrum row (config `spectrum`, default on). spectrum_enabled: bool, + /// Whether the server output is muted. + muted: bool, } impl Default for NowPlaying { @@ -67,6 +69,7 @@ impl Default for NowPlaying { track: None, spectrum: Vec::new(), spectrum_enabled: true, + muted: false, } } } @@ -119,6 +122,10 @@ impl NowPlaying { pub fn set_spectrum_enabled(&mut self, enabled: bool) { self.spectrum_enabled = enabled; } + /// Reflects the server's mute state. + pub fn update_mute(&mut self, muted: bool) { + self.muted = muted; + } pub fn render(&self, f: &mut Frame, area: Rect) { // With the spectrum on, the info block takes exactly the height @@ -154,8 +161,10 @@ impl NowPlaying { None => "No album".to_string(), }; let mods = format!( - "Shuffle: {}, Repeat {}", - self.modifiers.shuffle, self.modifiers.repeat + "Shuffle: {}, Repeat: {}{}", + self.modifiers.shuffle, + self.modifiers.repeat, + if self.muted { ", Muted" } else { "" }, ); vec![ Line::from(Span::raw(mods)), @@ -287,6 +296,7 @@ mod tests { }), spectrum: Vec::new(), spectrum_enabled: true, + muted: false, } } diff --git a/cbd-tui/src/lib.rs b/cbd-tui/src/lib.rs index 69fa887..e5ee9d6 100644 --- a/cbd-tui/src/lib.rs +++ b/cbd-tui/src/lib.rs @@ -258,7 +258,7 @@ fn run_ui(tx: Sender, rx: Receiver, spectrum_enabled StreamUpdate::Mods(mods) => { app.now_playing.update_modifiers(&mods); } - StreamUpdate::Mute(_) => { /* FIXME: implement */ } + StreamUpdate::Mute(muted) => app.now_playing.update_mute(muted), StreamUpdate::Volume(_) => { /* FIXME: implement */ } StreamUpdate::CaptureProgress(progress) => { app.captures.apply(progress); diff --git a/crabidy-server/src/playback.rs b/crabidy-server/src/playback.rs index 898e689..91f9d83 100644 --- a/crabidy-server/src/playback.rs +++ b/crabidy-server/src/playback.rs @@ -352,10 +352,13 @@ impl Playback { }; } - PlaybackCommand::ToggleMute => { - // FIXME: implement mute in the player engine - debug!("toggle mute requested (not implemented)"); - } + PlaybackCommand::ToggleMute => match self.player.toggle_mute().await { + Ok(muted) => { + debug!(muted, "toggled mute"); + self.broadcast(StreamUpdate::Mute(muted)); + } + Err(err) => warn!("toggle_mute failed: {err:?}"), + }, PlaybackCommand::Next => { let track = {