Skip to content

Commit 9c112c6

Browse files
committed
feat: add control for alerts volume/mute (fixes #1017)
Using an approach based on the System Sounds setting in pavucontrol, this gets and sets route-settings metadata that adjusts the volume of media with the Notification media-role.
1 parent 7251c8c commit 9c112c6

File tree

3 files changed

+176
-14
lines changed
  • cosmic-settings/src/pages/sound
  • crates/cosmic-pipewire/src
  • subscriptions/sound/src

3 files changed

+176
-14
lines changed

cosmic-settings/src/pages/sound/mod.rs

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ pub enum Message {
4747
ToggleOverAmplificationSink(bool),
4848
/// Toggle amplification for sink
4949
ToggleOverAmplificationSource(bool),
50+
/// Toggle the mute status of notifications.
51+
ToggleNotificationMute,
52+
/// Set the volume of notifications.
53+
SetNotificationVolume(u32),
5054
}
5155

5256
impl From<Message> for crate::pages::Message {
@@ -116,6 +120,7 @@ impl page::Page<crate::pages::Message> for Page {
116120
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
117121
) -> Option<page::Content> {
118122
Some(vec![
123+
sections.insert(alerts()),
119124
sections.insert(output()),
120125
sections.insert(input()),
121126
sections.insert(device_profiles()),
@@ -199,6 +204,8 @@ impl Page {
199204

200205
Message::ToggleSourceMute => self.model.toggle_source_mute(),
201206

207+
Message::ToggleNotificationMute => self.model.toggle_notification_mute(),
208+
202209
Message::SetSinkVolume(volume) => {
203210
return self
204211
.model
@@ -213,6 +220,13 @@ impl Page {
213220
.map(|message| Message::Subscription(message).into());
214221
}
215222

223+
Message::SetNotificationVolume(volume) => {
224+
return self
225+
.model
226+
.set_notification_volume(volume)
227+
.map(|message| Message::Subscription(message).into());
228+
}
229+
216230
Message::ToggleOverAmplificationSink(enabled) => {
217231
self.amplification_sink = enabled;
218232

@@ -459,21 +473,53 @@ fn device_profiles() -> Section<crate::pages::Message> {
459473
})
460474
}
461475

462-
// fn alerts() -> Section<crate::pages::Message> {
463-
// let mut descriptions = Slab::new();
464-
// let volume = descriptions.insert(fl!("sound-alerts", "volume"));
465-
// let sound = descriptions.insert(fl!("sound-alerts", "sound"));
476+
fn alerts() -> Section<crate::pages::Message> {
477+
let mut descriptions = Slab::new();
478+
let volume = descriptions.insert(fl!("sound-alerts", "volume"));
479+
let sound = descriptions.insert(fl!("sound-alerts", "sound"));
466480

467-
// Section::default()
468-
// .title(fl!("sound-alerts"))
469-
// .descriptions(descriptions)
470-
// .view::<Page>(move |_binder, _page, section| {
471-
// settings::section().title(&section.title)
472-
// .add(settings::item(&section.descriptions[volume], text::body("TODO")))
473-
// .add(settings::item(&section.descriptions[sound], text::body("TODO")))
474-
// .into()
475-
// })
476-
// }
481+
Section::default()
482+
.title(fl!("sound-alerts"))
483+
.descriptions(descriptions)
484+
.view::<Page>(move |_binder, page, section| {
485+
let slider = if page.amplification_sink {
486+
widget::slider(0..=150, page.model.notification_volume, |change| {
487+
Message::SetNotificationVolume(change).into()
488+
})
489+
.breakpoints(&[100])
490+
} else {
491+
widget::slider(0..=100, page.model.notification_volume, |change| {
492+
Message::SetNotificationVolume(change).into()
493+
})
494+
};
495+
496+
let volume_control = widget::row::with_capacity(4)
497+
.align_y(Alignment::Center)
498+
.push(
499+
widget::button::icon(if page.model.notification_mute {
500+
widget::icon::from_name("audio-volume-muted-symbolic")
501+
} else {
502+
widget::icon::from_name("audio-volume-high-symbolic")
503+
})
504+
.on_press(Message::ToggleNotificationMute.into()),
505+
)
506+
.push(
507+
widget::text::body(&page.model.notification_volume_text)
508+
.width(Length::Fixed(22.0))
509+
.align_x(Alignment::Center),
510+
)
511+
.push(widget::horizontal_space().width(8))
512+
.push(slider);
513+
514+
settings::section()
515+
.title(&section.title)
516+
.add(settings::flex_item(
517+
&*section.descriptions[volume],
518+
volume_control,
519+
))
520+
.into()
521+
})
522+
}
477523

478524
// fn applications() -> Section<crate::pages::Message> {
479525
// let mut descriptions = Slab::new();

crates/cosmic-pipewire/src/lib.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ fn run_service(
7777
node_props: IntMap::new(),
7878
main_loop: main_loop.downgrade(),
7979
on_event: Box::new(on_event),
80+
route_settings_metadata_id: None,
8081
}));
8182

8283
let _request_handler = rx.attach(main_loop.loop_(), {
@@ -100,6 +101,12 @@ fn run_service(
100101
}
101102
}
102103

104+
Request::SetNotification(volume, mute) => {
105+
if let Some(state) = state.upgrade() {
106+
state.borrow_mut().set_notification_settings(volume, mute);
107+
}
108+
}
109+
103110
Request::SetProfile(id, index, save) => {
104111
if let Some(state) = state.upgrade() {
105112
state.borrow_mut().set_profile(id, index, save);
@@ -332,6 +339,14 @@ fn run_service(
332339

333340
let id = metadata.upcast_ref().id();
334341

342+
if obj
343+
.props
344+
.and_then(|props| props.get("metadata.name"))
345+
.is_some_and(|name| name == "route-settings")
346+
{
347+
state.borrow_mut().route_settings_metadata_id = Some(id);
348+
}
349+
335350
let listener = metadata
336351
.add_listener_local()
337352
.property({
@@ -366,6 +381,26 @@ fn run_service(
366381
}
367382
}
368383

384+
"restore.stream.Output/Audio.media.role:Notification" => {
385+
if let Ok(metadata) =
386+
serde_json::de::from_str::<NotificationRouteSettingsMetadata>(
387+
value,
388+
)
389+
{
390+
if let Some(state) = state.upgrade() {
391+
let volume = metadata
392+
.volumes
393+
.first()
394+
.copied()
395+
.unwrap_or(1.0)
396+
.powf(1.0 / 3.0);
397+
state.borrow_mut().on_event(Event::NotificationVolume(
398+
volume, metadata.mute,
399+
));
400+
}
401+
}
402+
}
403+
369404
_ => (),
370405
}
371406

@@ -421,6 +456,8 @@ pub enum Event {
421456
DefaultSink(String),
422457
/// The default source was changed.
423458
DefaultSource(String),
459+
/// The notification volume/mute state changed.
460+
NotificationVolume(f32, bool),
424461
/// Emitted when the properties of a node has changed.
425462
NodeProperties(NodeId, NodeProps),
426463
/// A device with the given device_id was removed.
@@ -439,6 +476,8 @@ pub enum Request {
439476
SetProfile(DeviceId, u32, bool),
440477
/// Set a new volume
441478
SetNodeVolume(DeviceId, f32, Option<f32>),
479+
/// Set the notification volume and mute state
480+
SetNotification(f32, bool),
442481
/// Stop the main loop and exit the thread.
443482
Quit,
444483
}
@@ -463,6 +502,12 @@ pub struct DefaultAudio<'a> {
463502
name: &'a str,
464503
}
465504

505+
#[derive(serde::Deserialize)]
506+
struct NotificationRouteSettingsMetadata {
507+
mute: bool,
508+
volumes: Vec<f32>,
509+
}
510+
466511
struct Proxies {
467512
devices: IntMap<
468513
PipewireId,
@@ -494,6 +539,7 @@ struct State {
494539
main_loop: MainLoopWeak,
495540
/// Handle events and exit the loop when `true` is returned.
496541
on_event: Box<dyn FnMut(Event)>,
542+
route_settings_metadata_id: Option<u32>,
497543
}
498544

499545
impl State {
@@ -860,6 +906,28 @@ impl State {
860906
}
861907
}
862908

909+
fn set_notification_settings(&self, volume: f32, mute: bool) {
910+
if let Some((metadata, ..)) = self
911+
.route_settings_metadata_id
912+
.map(|id| self.proxies.metadata.get(id))
913+
.flatten()
914+
{
915+
metadata.set_property(
916+
0,
917+
"restore.stream.Output/Audio.media.role:Notification",
918+
Some("Spa:String:JSON"),
919+
Some(
920+
&serde_json::json!({
921+
"mute": mute,
922+
"volumes": [volume.powi(3)],
923+
"channels": ["MONO"]
924+
})
925+
.to_string(),
926+
),
927+
);
928+
}
929+
}
930+
863931
fn device(&self, id: DeviceId) -> Option<&pipewire::device::Device> {
864932
self.proxies
865933
.devices

subscriptions/sound/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,19 @@ pub struct Model {
114114

115115
pub sink_volume_text: String,
116116
pub source_volume_text: String,
117+
pub notification_volume_text: String,
117118
pub sink_balance: Option<f32>,
118119

119120
pub sink_volume: u32,
120121
pub source_volume: u32,
122+
pub notification_volume: u32,
121123

122124
pub sink_mute: bool,
123125
sink_volume_debounce: bool,
124126
pub source_mute: bool,
125127
source_volume_debounce: bool,
128+
pub notification_mute: bool,
129+
notification_volume_debounce: bool,
126130
}
127131

128132
impl Model {
@@ -354,6 +358,28 @@ impl Model {
354358
Task::none()
355359
}
356360

361+
pub fn set_notification_volume(&mut self, volume: u32) -> Task<Message> {
362+
self.notification_volume = volume;
363+
self.notification_volume_text = numtoa::BaseN::<10>::u32(volume).as_str().to_owned();
364+
if self.notification_volume_debounce {
365+
return Task::none();
366+
}
367+
368+
self.notification_volume_debounce = true;
369+
return cosmic::Task::future(async move {
370+
tokio::time::sleep(Duration::from_millis(128)).await;
371+
Message::NotificationVolumeApply().into()
372+
});
373+
}
374+
375+
pub fn toggle_notification_mute(&mut self) {
376+
self.notification_mute = !self.notification_mute;
377+
self.pipewire_send(pipewire::Request::SetNotification(
378+
self.notification_volume as f32 / 100.0,
379+
self.notification_mute,
380+
));
381+
}
382+
357383
pub fn update(&mut self, message: Message) -> Task<Message> {
358384
match message {
359385
Message::Server(events) => {
@@ -381,6 +407,15 @@ impl Model {
381407
));
382408
}
383409

410+
Message::NotificationVolumeApply() => {
411+
self.notification_volume_debounce = false;
412+
let new_volume = self.notification_volume as f32 / 100.0;
413+
self.pipewire_send(pipewire::Request::SetNotification(
414+
new_volume,
415+
self.notification_mute,
416+
));
417+
}
418+
384419
Message::SubHandle(handle) => {
385420
if let Some(handle) = Arc::into_inner(handle) {
386421
self.subscription_handle = Some(handle);
@@ -393,6 +428,17 @@ impl Model {
393428

394429
fn pipewire_update(&mut self, event: pipewire::Event) {
395430
match event {
431+
pipewire::Event::NotificationVolume(volume, mute) => {
432+
if self.notification_volume_debounce {
433+
return;
434+
}
435+
436+
self.notification_mute = mute;
437+
self.notification_volume = (volume * 100.0) as u32;
438+
self.notification_volume_text = numtoa::BaseN::<10>::u32(self.notification_volume)
439+
.as_str()
440+
.to_owned();
441+
}
396442
pipewire::Event::NodeProperties(id, props) => {
397443
if self.active_sink_node == Some(id) {
398444
if self.sink_volume_debounce {
@@ -849,6 +895,8 @@ pub enum Message {
849895
SinkVolumeApply(NodeId),
850896
/// Change the input volume.
851897
SourceVolumeApply(NodeId),
898+
/// Change the notification volume.
899+
NotificationVolumeApply(),
852900
/// On init of the subscription, channels for closing background threads are given to the app.
853901
SubHandle(Arc<SubscriptionHandle>),
854902
}

0 commit comments

Comments
 (0)