diff --git a/cosmic-settings/src/pages/sound/mod.rs b/cosmic-settings/src/pages/sound/mod.rs index 2ad1974da..cca57890e 100644 --- a/cosmic-settings/src/pages/sound/mod.rs +++ b/cosmic-settings/src/pages/sound/mod.rs @@ -34,6 +34,7 @@ pub enum Message { SetSinkVolume(u32), /// Request to change the input volume. SetSourceVolume(u32), + TestOutput, /// Messages handled by the sound module in cosmic-settings-subscriptions Subscription(subscription::Message), /// Surface Action @@ -212,6 +213,10 @@ impl Page { .map(|message| Message::Subscription(message).into()); } + Message::TestOutput => { + self.model.test_output(); + } + Message::ToggleOverAmplificationSink(enabled) => { self.amplification_sink = enabled; @@ -334,6 +339,7 @@ fn output() -> Section { crate::slab!(descriptions { volume = fl!("sound-output", "volume"); device = fl!("sound-output", "device"); + test = fl!("sound-output", "test"); _level = fl!("sound-output", "level"); balance = fl!("sound-output", "balance"); left = fl!("sound-output", "left"); @@ -389,6 +395,15 @@ fn output() -> Section { .apply(Element::from) .map(crate::pages::Message::from); + let test_output = widget::button::standard(&*section.descriptions[test]) + .on_press_maybe(page.model.active_sink().map(|_| Message::TestOutput.into())); + + let output_device_controls = widget::row::with_capacity(3) + .align_y(Alignment::Center) + .push(devices) + .push(horizontal_space().width(8.)) + .push(test_output); + let mut controls = settings::section() .title(§ion.title) .add( @@ -396,7 +411,10 @@ fn output() -> Section { .flex_control(volume_control) .align_items(Alignment::Center), ) - .add(settings::item(&*section.descriptions[device], devices)) + .add(settings::item( + &*section.descriptions[device], + output_device_controls, + )) .add(settings::item( &*section.descriptions[balance], widget::row::with_capacity(4) diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 735d89223..e8305ef8e 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -588,6 +588,7 @@ sound = Sound sound-output = Output .volume = Output volume .device = Output device + .test = Test .level = Output level .config = Configuration .balance = Balance diff --git a/subscriptions/sound/src/lib.rs b/subscriptions/sound/src/lib.rs index e1b9e3918..6aeb60cb7 100644 --- a/subscriptions/sound/src/lib.rs +++ b/subscriptions/sound/src/lib.rs @@ -138,6 +138,19 @@ impl Model { &self.sources } + pub fn test_output(&self) { + if self.active_sink_node.is_none() { + tracing::warn!(target: "sound", "cannot play output test sound without an active sink"); + return; + } + + tokio::spawn(async { + if !play_output_test_sound().await { + tracing::warn!(target: "sound", "failed to play output test sound using available backends"); + } + }); + } + pub fn clear(&mut self) { if let Some(handle) = self.subscription_handle.take() { _ = handle.cancel_tx.send(()); @@ -956,3 +969,56 @@ pub async fn set_profile(id: u32, index: u32, save: bool) { .status() .await; } + +async fn play_output_test_sound() -> bool { + if run_command("canberra-gtk-play", &["-i", "audio-test-signal"]).await { + return true; + } + + let test_sound_paths = [ + "/usr/share/sounds/freedesktop/stereo/audio-test-signal.oga", + "/usr/share/sounds/freedesktop/stereo/audio-test-signal.ogg", + "/usr/share/sounds/freedesktop/stereo/audio-test-signal.wav", + "/usr/share/sounds/freedesktop/stereo/bell.oga", + "/usr/share/sounds/freedesktop/stereo/bell.ogg", + "/usr/share/sounds/freedesktop/stereo/bell.wav", + "/usr/share/sounds/freedesktop/stereo/audio-test.ogg", + ]; + + for sound_path in test_sound_paths { + if run_command("paplay", &[sound_path]).await { + return true; + } + } + + for sound_path in test_sound_paths { + if run_command("pw-play", &[sound_path]).await { + return true; + } + } + + false +} + +async fn run_command(command: &str, args: &[&str]) -> bool { + match tokio::time::timeout( + Duration::from_secs(5), + tokio::process::Command::new(command) + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(), + ) + .await + { + Ok(Ok(status)) => status.success(), + Ok(Err(why)) => { + tracing::debug!(target: "sound", ?why, command, "failed to run test sound command"); + false + } + Err(why) => { + tracing::warn!(target: "sound", ?why, command, "test sound command timed out"); + false + } + } +}