From 80c4b6e7ed11f9b661cfa7357736d4af006b6970 Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 22 Jul 2026 00:20:01 +0200 Subject: [PATCH] Implement mute (it was a stub) Toggling mute did nothing: the server logged a FIXME and never touched the player, and the TUI ignored the Mute stream update. Now the player engine mutes by zeroing the sink volume and remembering the level to restore (setting the volume unmutes), ToggleMute drives it and broadcasts the new state, the TUI shows a Muted marker in the now-playing pane, and the web client mute button already reflected the Mute update so it now works too. Co-Authored-By: Claude Opus 4.8 (1M context) --- audio-player/src/player.rs | 9 ++++++++ audio-player/src/player_engine.rs | 38 ++++++++++++++++++++++++++++--- cbd-tui/src/app/now_playing.rs | 14 ++++++++++-- cbd-tui/src/lib.rs | 2 +- crabidy-server/src/playback.rs | 11 +++++---- 5 files changed, 64 insertions(+), 10 deletions(-) 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 = {