From ddbe8577cc3b8b0265217b8c0735328dc440a97c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 15 Aug 2024 19:33:48 +0200 Subject: [PATCH] fix(sound): profile mismatch and device shuffling --- Cargo.lock | 29 ++--- cosmic-settings/Cargo.toml | 1 + cosmic-settings/src/pages/sound.rs | 177 ++++++++++++++++++++++------- 3 files changed, 150 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5256780..3eb48b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1503,7 +1503,7 @@ source = "git+https://github.com/pop-os/cosmic-randr#71fabbb382fa8cf750f50fb77c4 dependencies = [ "cosmic-protocols", "futures-lite 2.3.0", - "indexmap 2.2.6", + "indexmap 2.4.0", "tachyonix", "thiserror", "tokio", @@ -1557,6 +1557,7 @@ dependencies = [ "i18n-embed-fl", "icu", "image 0.25.2", + "indexmap 2.4.0", "itertools 0.13.0", "itoa", "libcosmic", @@ -1805,7 +1806,7 @@ version = "0.19.0" source = "git+https://github.com/gfx-rs/wgpu?rev=20fda69#20fda698341efbdc870b8027d6d49f5bf3f36109" dependencies = [ "bitflags 2.6.0", - "libloading 0.7.4", + "libloading 0.8.5", "winapi", ] @@ -1960,7 +1961,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.7.4", + "libloading 0.8.5", ] [[package]] @@ -2829,7 +2830,7 @@ dependencies = [ "bitflags 2.6.0", "com", "libc", - "libloading 0.7.4", + "libloading 0.8.5", "thiserror", "widestring", "winapi", @@ -3711,9 +3712,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -4474,7 +4475,7 @@ dependencies = [ "bitflags 2.6.0", "codespan-reporting", "hexf-parse", - "indexmap 2.2.6", + "indexmap 2.4.0", "log", "num-traits", "rustc-hash", @@ -5884,7 +5885,7 @@ version = "1.0.121" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "itoa", "memchr", "ryu", @@ -5921,7 +5922,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_derive", "serde_json", @@ -6676,7 +6677,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "toml_datetime", "winnow 0.5.40", ] @@ -6687,7 +6688,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "toml_datetime", "winnow 0.5.40", ] @@ -6698,7 +6699,7 @@ version = "0.22.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1490595c74d930da779e944f5ba2ecdf538af67df1a9848cbd156af43c1b7cf0" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "serde", "serde_spanned", "toml_datetime", @@ -7411,7 +7412,7 @@ dependencies = [ "bitflags 2.6.0", "cfg_aliases 0.1.1", "codespan-reporting", - "indexmap 2.2.6", + "indexmap 2.4.0", "log", "naga", "once_cell", @@ -7449,7 +7450,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.7.4", + "libloading 0.8.5", "log", "metal", "naga", diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 6adbe25..1a2c094 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -54,6 +54,7 @@ xkb-data = "0.1.0" zbus = { version = "4.4.0", features = ["tokio"] } tachyonix = "0.3.0" slab = "0.4.9" +indexmap = "2.4.0" [dependencies.cosmic-settings-subscriptions] git = "https://github.com/pop-os/cosmic-settings-subscriptions" diff --git a/cosmic-settings/src/pages/sound.rs b/cosmic-settings/src/pages/sound.rs index a538f9a..42d456b 100644 --- a/cosmic-settings/src/pages/sound.rs +++ b/cosmic-settings/src/pages/sound.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: GPL-3.0-only -use std::{collections::BTreeMap, time::Duration}; +use std::time::Duration; use cosmic::{ widget::{self, settings}, @@ -10,9 +10,13 @@ use cosmic::{ use cosmic_settings_page::{self as page, section, Section}; use cosmic_settings_subscriptions::{pipewire, pulse}; use futures::StreamExt; +use indexmap::IndexMap; use slab::Slab; use slotmap::SlotMap; +pub type NodeId = u32; +pub type ProfileId = u32; + #[derive(Clone, Debug)] pub enum Message { /// Get default sinks/sources and their volumes/mute status. @@ -23,6 +27,8 @@ pub enum Message { SinkChanged(usize), /// Change the active profile for an output. SinkProfileChanged(usize), + /// Select a device from the given card after a profile change. + SinkProfileSelect(DeviceId), /// Request to change the default output volume. SinkVolumeChanged(u32), /// Change the output volume. @@ -33,6 +39,8 @@ pub enum Message { SourceChanged(usize), /// Change the active profile for an output. SourceProfileChanged(usize), + /// Select a device from the given card after a profile change. + SourceProfileSelect(DeviceId), /// Request to change the input volume. SourceVolumeChanged(u32), /// Change the input volume. @@ -41,22 +49,20 @@ pub enum Message { SourceMuteToggle, } -pub type NodeId = u32; -pub type ProfileId = u32; - #[derive(Debug)] struct Card { - devices: BTreeMap, + devices: IndexMap, } #[derive(Debug)] -struct Port { +struct Device { class: pipewire::MediaClass, identifier: String, + description: String, } #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] -enum DeviceId { +pub enum DeviceId { Alsa(u32), Bluez5(String), } @@ -65,11 +71,10 @@ enum DeviceId { pub struct Page { pipewire_thread: Option<(tokio::sync::oneshot::Sender<()>, pipewire::Sender<()>)>, pulse_thread: Option>, - devices: BTreeMap, - card_names: BTreeMap, - card_ports: BTreeMap>, - card_profiles: BTreeMap>, - active_profiles: BTreeMap>, + devices: IndexMap, + card_names: IndexMap, + card_profiles: IndexMap>, + active_profiles: IndexMap>, default_sink: String, default_source: String, @@ -99,6 +104,9 @@ pub struct Page { active_source: Option, active_source_device: Option, active_source_profile: Option, + + changing_sink_profile: bool, + changing_source_profile: bool, } impl page::Page for Page { @@ -234,7 +242,16 @@ impl Page { ) = self.device_profiles(device_id); } - fn set_default_sink(&mut self) { + fn set_default_sink(&mut self, sink: String) { + if self.default_sink == sink { + return; + } + + self.default_sink = sink; + self.active_sink_profile = None; + self.sink_profiles.clear(); + self.sink_profile_names.clear(); + for (device_id, card) in &self.devices { for (&node_id, device) in &card.devices { if let pipewire::MediaClass::Sink = device.class { @@ -250,7 +267,16 @@ impl Page { } } - fn set_default_source(&mut self) { + fn set_default_source(&mut self, source: String) { + if self.default_source == source { + return; + } + + self.default_source = source; + self.active_source_profile = None; + self.source_profiles.clear(); + self.source_profile_names.clear(); + for (device_id, card) in &self.devices { for (&node_id, device) in &card.devices { if let pipewire::MediaClass::Source = device.class { @@ -329,13 +355,15 @@ impl Page { } Message::Pulse(pulse::Event::DefaultSink(sink)) => { - self.default_sink = sink; - self.set_default_sink(); + if !self.changing_sink_profile { + self.set_default_sink(sink); + } } Message::Pulse(pulse::Event::DefaultSource(source)) => { - self.default_source = source; - self.set_default_source(); + if !self.changing_source_profile { + self.set_default_source(source); + } } Message::Pulse(pulse::Event::SinkMute(mute)) => { @@ -353,7 +381,6 @@ impl Page { }; self.card_names.insert(device_id.clone(), card.name); - self.card_ports.insert(device_id.clone(), card.ports); self.card_profiles.insert(device_id.clone(), card.profiles); self.active_profiles .insert(device_id, card.active_profile.map(|p| p.name)); @@ -369,8 +396,12 @@ impl Page { pipewire::MediaClass::Sink => { self.sinks.push(device.node_description.clone()); self.sink_ids.push(device.object_id); + sort_pulse_devices(&mut self.sinks, &mut self.sink_ids); if self.default_sink == device.node_name { - self.active_sink = Some(self.sinks.len() - 1); + self.active_sink = self + .sinks + .iter() + .position(|s| *s == device.node_description); self.active_sink_device = Some(device_id.clone()); self.set_sink_profiles(&device_id); } @@ -379,8 +410,12 @@ impl Page { pipewire::MediaClass::Source => { self.sources.push(device.node_description.clone()); self.source_ids.push(device.object_id); + sort_pulse_devices(&mut self.sources, &mut self.source_ids); if self.default_source == device.node_name { - self.active_source = Some(self.sources.len() - 1); + self.active_source = self + .sources + .iter() + .position(|s| *s == device.node_description); self.active_source_device = Some(device_id.clone()); self.set_source_profiles(&device_id); } @@ -388,16 +423,20 @@ impl Page { } let card = self.devices.entry(device_id).or_insert_with(|| Card { - devices: BTreeMap::new(), + devices: IndexMap::new(), }); card.devices.insert( device.object_id, - Port { + Device { class: device.media_class, identifier: device.node_name, + description: device.node_description, }, ); + + card.devices + .sort_unstable_by(|_, av, _, bv| av.description.cmp(&bv.description)); } Message::Pipewire(pipewire::DeviceEvent::Remove(node_id)) => { @@ -412,7 +451,7 @@ impl Page { } if let Some(card_id) = remove { - _ = self.devices.remove(&card_id); + _ = self.devices.shift_remove(&card_id); } if let Some(pos) = self.sink_ids.iter().position(|&id| id == node_id) { @@ -441,6 +480,8 @@ impl Page { if node_id == nid { self.active_sink = Some(pos); pactl_set_default_sink(device.identifier.clone()); + self.set_default_sink(device.identifier.clone()); + return Command::none(); } } } @@ -454,6 +495,8 @@ impl Page { if node_id == nid { self.active_source = Some(pos); pactl_set_default_source(device.identifier.clone()); + self.set_default_source(device.identifier.clone()); + return Command::none(); } } } @@ -486,29 +529,68 @@ impl Page { Message::SinkProfileChanged(profile) => { self.active_sink_profile = Some(profile); - if let Some(profile) = self.sink_profile_names.get(profile) { - if let Some(ref device_id) = self.active_sink_device { - if let Some(name) = self.card_names.get(device_id) { - pactl_set_card_profile(name.clone(), profile.clone()); + + if let Some(profile) = self.sink_profile_names.get(profile).cloned() { + if let Some(device_id) = self.active_sink_device.clone() { + if let Some(name) = self.card_names.get(&device_id).cloned() { self.active_profiles .insert(device_id.clone(), Some(profile.clone())); + + self.changing_sink_profile = true; + return cosmic::command::future(async move { + pactl_set_card_profile(name, profile).await; + Message::SinkProfileSelect(device_id) + }) + .map(crate::pages::Message::Sound) + .map(crate::app::Message::PageMessage); } } } } + Message::SinkProfileSelect(device_id) => { + self.changing_sink_profile = false; + let sink_pos = self.active_sink.unwrap_or(0); + + if let Some(card) = self.devices.get(&device_id) { + if let Some((_, device)) = card.devices.get_index(sink_pos) { + pactl_set_default_sink(device.identifier.clone()); + self.set_default_sink(device.identifier.clone()); + } + } + } + Message::SourceProfileChanged(profile) => { self.active_source_profile = Some(profile); - if let Some(profile) = self.source_profile_names.get(profile) { - if let Some(ref device_id) = self.active_source_device { - if let Some(name) = self.card_names.get(device_id) { - pactl_set_card_profile(name.clone(), profile.clone()); + if let Some(profile) = self.source_profile_names.get(profile).cloned() { + if let Some(device_id) = self.active_source_device.clone() { + if let Some(name) = self.card_names.get(&device_id).cloned() { self.active_profiles .insert(device_id.clone(), Some(profile.clone())); + + self.changing_source_profile = true; + return cosmic::command::future(async move { + pactl_set_card_profile(name, profile).await; + Message::SourceProfileSelect(device_id) + }) + .map(crate::pages::Message::Sound) + .map(crate::app::Message::PageMessage); } } } } + + Message::SourceProfileSelect(device_id) => { + self.changing_source_profile = false; + let source_pos = self.active_source.unwrap_or(0); + + if let Some(card) = self.devices.get(&device_id) { + if let Some((_, device)) = card.devices.get_index(source_pos) { + pactl_set_default_source(device.identifier.clone()); + self.set_default_source(device.identifier.clone()); + } + } + } } Command::none() } @@ -530,11 +612,11 @@ fn input() -> Section { .align_items(cosmic::iced::Alignment::Center) .spacing(4) .push( - widget::button::icon(if page.source_mute { - widget::icon::from_name("microphone-sensitivity-muted-symbolic") + widget::button::icon(widget::icon::from_name(if page.source_mute { + "microphone-sensitivity-muted-symbolic" } else { - widget::icon::from_name("audio-input-microphone-symbolic") - }) + "audio-input-microphone-symbolic" + })) .on_press(Message::SourceMuteToggle), ) .push(widget::text::body(&page.source_volume_text)) @@ -663,13 +745,22 @@ fn output() -> Section { // }) // } -fn pactl_set_card_profile(id: String, profile: String) { - tokio::task::spawn(async move { - _ = tokio::process::Command::new("pactl") - .args(&["set-card-profile", id.as_str(), profile.as_str()]) - .status() - .await - }); +fn sort_pulse_devices(descriptions: &mut Vec, node_ids: &mut Vec) { + let mut tmp: Vec<(String, NodeId)> = std::mem::take(descriptions) + .into_iter() + .zip(std::mem::take(node_ids).into_iter()) + .collect(); + + tmp.sort_unstable_by(|(ak, _), (bk, _)| ak.cmp(&bk)); + + (*descriptions, *node_ids) = tmp.into_iter().collect(); +} + +async fn pactl_set_card_profile(id: String, profile: String) { + _ = tokio::process::Command::new("pactl") + .args(&["set-card-profile", id.as_str(), profile.as_str()]) + .status() + .await } fn pactl_set_default_sink(id: String) {