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) <noreply@anthropic.com>
This commit is contained in:
Test User 2026-07-22 00:20:01 +02:00
parent 7f34f869a9
commit 80c4b6e7ed
5 changed files with 64 additions and 10 deletions

View File

@ -124,6 +124,15 @@ impl Player {
Ok(rx.recv_async().await?) Ok(rx.recv_async().await?)
} }
/// Toggles mute; resolves to the new muted state.
pub async fn toggle_mute(&self) -> Result<bool> {
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<()> { pub async fn pause(&self) -> Result<()> {
let (tx, rx) = flume::bounded(1); let (tx, rx) = flume::bounded(1);
self.tx_engine self.tx_engine

View File

@ -47,6 +47,7 @@ pub enum PlayerEngineCommand {
GetElapsed(Sender<Result<Duration>>), GetElapsed(Sender<Result<Duration>>),
SeekTo(Duration, Sender<Result<Duration>>), SeekTo(Duration, Sender<Result<Duration>>),
GetVolume(Sender<f32>), GetVolume(Sender<f32>),
ToggleMute(Sender<bool>),
GetPaused(Sender<Result<bool>>), GetPaused(Sender<Result<bool>>),
/// End of stream for the source started by the given generation. /// End of stream for the source started by the given generation.
/// Stale generations are ignored so an old track finishing can never /// 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 /// visualizer (architecture/spectrum.md). Handed out via
/// [`Self::spectrum_tap`] so the server's FFT task can read it. /// [`Self::spectrum_tap`] so the server's FFT task can read it.
spectrum: Arc<SpectrumTap>, spectrum: Arc<SpectrumTap>,
/// Whether output is muted (sink volume zeroed).
muted: bool,
/// Volume to restore on unmute.
pre_mute_volume: f32,
} }
impl PlayerEngine { impl PlayerEngine {
@ -140,6 +145,8 @@ impl PlayerEngine {
_owned_runtime: owned_runtime, _owned_runtime: owned_runtime,
http, http,
spectrum, spectrum,
muted: false,
pre_mute_volume: 1.0,
}) })
} }
@ -174,6 +181,7 @@ impl PlayerEngine {
send_reply(tx, self.set_volume(volume)); send_reply(tx, self.set_volume(volume));
} }
PlayerEngineCommand::GetVolume(tx) => send_reply(tx, self.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::GetPaused(tx) => send_reply(tx, self.is_paused()),
PlayerEngineCommand::Eos(generation) => self.handle_eos(generation), PlayerEngineCommand::Eos(generation) => self.handle_eos(generation),
} }
@ -386,13 +394,37 @@ impl PlayerEngine {
Ok(self.sink.get_pos()) 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 { 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() self.sink.volume()
} }
pub fn set_volume(&mut self, volume: f32) -> f32 { /// Toggles mute by zeroing the sink volume and remembering the level
self.sink.set_volume(volume.clamp(0.0, 1.1)); /// to restore. Returns the new muted state.
self.sink.volume() 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) { fn handle_eos(&mut self, generation: u64) {

View File

@ -55,6 +55,8 @@ pub struct NowPlaying {
spectrum: Vec<f32>, spectrum: Vec<f32>,
/// Whether to draw the spectrum row (config `spectrum`, default on). /// Whether to draw the spectrum row (config `spectrum`, default on).
spectrum_enabled: bool, spectrum_enabled: bool,
/// Whether the server output is muted.
muted: bool,
} }
impl Default for NowPlaying { impl Default for NowPlaying {
@ -67,6 +69,7 @@ impl Default for NowPlaying {
track: None, track: None,
spectrum: Vec::new(), spectrum: Vec::new(),
spectrum_enabled: true, spectrum_enabled: true,
muted: false,
} }
} }
} }
@ -119,6 +122,10 @@ impl NowPlaying {
pub fn set_spectrum_enabled(&mut self, enabled: bool) { pub fn set_spectrum_enabled(&mut self, enabled: bool) {
self.spectrum_enabled = enabled; 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) { pub fn render(&self, f: &mut Frame, area: Rect) {
// With the spectrum on, the info block takes exactly the height // With the spectrum on, the info block takes exactly the height
@ -154,8 +161,10 @@ impl NowPlaying {
None => "No album".to_string(), None => "No album".to_string(),
}; };
let mods = format!( let mods = format!(
"Shuffle: {}, Repeat {}", "Shuffle: {}, Repeat: {}{}",
self.modifiers.shuffle, self.modifiers.repeat self.modifiers.shuffle,
self.modifiers.repeat,
if self.muted { ", Muted" } else { "" },
); );
vec![ vec![
Line::from(Span::raw(mods)), Line::from(Span::raw(mods)),
@ -287,6 +296,7 @@ mod tests {
}), }),
spectrum: Vec::new(), spectrum: Vec::new(),
spectrum_enabled: true, spectrum_enabled: true,
muted: false,
} }
} }

View File

@ -258,7 +258,7 @@ fn run_ui(tx: Sender<MessageFromUi>, rx: Receiver<MessageToUi>, spectrum_enabled
StreamUpdate::Mods(mods) => { StreamUpdate::Mods(mods) => {
app.now_playing.update_modifiers(&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::Volume(_) => { /* FIXME: implement */ }
StreamUpdate::CaptureProgress(progress) => { StreamUpdate::CaptureProgress(progress) => {
app.captures.apply(progress); app.captures.apply(progress);

View File

@ -352,10 +352,13 @@ impl Playback {
}; };
} }
PlaybackCommand::ToggleMute => { PlaybackCommand::ToggleMute => match self.player.toggle_mute().await {
// FIXME: implement mute in the player engine Ok(muted) => {
debug!("toggle mute requested (not implemented)"); debug!(muted, "toggled mute");
self.broadcast(StreamUpdate::Mute(muted));
} }
Err(err) => warn!("toggle_mute failed: {err:?}"),
},
PlaybackCommand::Next => { PlaybackCommand::Next => {
let track = { let track = {