fix(sound): profile mismatch and device shuffling

This commit is contained in:
Michael Aaron Murphy 2024-08-15 19:33:48 +02:00
parent 67a5a9852c
commit ddbe8577cc
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
3 changed files with 150 additions and 57 deletions

29
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -1,7 +1,7 @@
// Copyright 2023 System76 <info@system76.com>
// 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<NodeId, Port>,
devices: IndexMap<NodeId, Device>,
}
#[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<tokio::sync::oneshot::Sender<()>>,
devices: BTreeMap<DeviceId, Card>,
card_names: BTreeMap<DeviceId, String>,
card_ports: BTreeMap<DeviceId, Vec<pulse::CardPort>>,
card_profiles: BTreeMap<DeviceId, Vec<pulse::CardProfile>>,
active_profiles: BTreeMap<DeviceId, Option<String>>,
devices: IndexMap<DeviceId, Card>,
card_names: IndexMap<DeviceId, String>,
card_profiles: IndexMap<DeviceId, Vec<pulse::CardProfile>>,
active_profiles: IndexMap<DeviceId, Option<String>>,
default_sink: String,
default_source: String,
@ -99,6 +104,9 @@ pub struct Page {
active_source: Option<usize>,
active_source_device: Option<DeviceId>,
active_source_profile: Option<usize>,
changing_sink_profile: bool,
changing_source_profile: bool,
}
impl page::Page<crate::pages::Message> 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<crate::pages::Message> {
.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<crate::pages::Message> {
// })
// }
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<String>, node_ids: &mut Vec<NodeId>) {
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) {