feat(sound): redesign with separate device profiles page (#1500)

This commit is contained in:
Michael Murphy 2025-11-25 21:46:17 +01:00 committed by GitHub
parent 6ebc2208ed
commit 2c9f60cd5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 3179 additions and 1971 deletions

View file

@ -0,0 +1,27 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use pipewire::device::DeviceInfoRef;
/// Device information
#[must_use]
#[derive(Clone, Debug)]
pub struct Device {
pub id: u32,
pub name: String,
}
impl Device {
/// Attains process info from a pipewire info node.
#[must_use]
pub fn from_device(info: &DeviceInfoRef) -> Option<Self> {
let props = info.props()?;
let device = Device {
id: props.get("object.id")?.parse::<u32>().ok()?,
name: props.get("device.description")?.to_owned(),
};
Some(device)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,190 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{Channel, spa_utils::array_from_pod};
use libspa::{pod::Pod, utils::Id};
use pipewire::node::{NodeInfoRef, NodeState};
use std::ffi::c_float;
/// Node information
#[must_use]
#[derive(Clone, Debug)]
pub struct Node {
pub object_id: u32,
pub audio_channels: u32,
pub audio_position: String,
pub card_profile_device: Option<u32>,
pub description: String,
pub device_id: Option<u32>,
pub device_profile_description: String,
pub device_profile_pro: bool,
pub icon_name: String,
pub media_class: MediaClass,
pub node_name: String,
pub state: State,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum State {
Idle,
Running,
Creating,
Suspended,
Error(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MediaClass {
Source,
Sink,
}
impl Node {
/// Attains process info from a pipewire info node.
#[must_use]
pub fn from_node(info: &NodeInfoRef) -> Option<Self> {
let props = info.props()?;
let mut audio_channels = 1;
let mut audio_position = String::new();
let mut card_profile_device = None;
let mut device_id = None;
let mut device_profile_description: &str = "";
let mut device_profile_pro = false;
let mut icon_name = String::new();
let mut media_class = None;
let mut node_description: &str = "";
let mut node_name = String::new();
let mut object_id = None;
for (entry, value) in props.iter() {
match entry {
"device.id" => device_id = value.parse::<u32>().ok(),
"object.id" => object_id = Some(value.parse::<u32>().ok()?),
// 2
"audio.channels" => audio_channels = value.parse::<u32>().unwrap_or(1),
// FL,FR
"audio.position" => audio_position = value.to_owned(),
// 0
"card.profile.device" => card_profile_device = Some(value.parse::<u32>().ok()?),
// Analog Stereo (ALSA only)
"device.profile.description" => {
device_profile_description = value;
}
// false
"device.profile.pro" => device_profile_pro = value == "true",
// audio-card-analog
"device.icon-name" => icon_name = value.to_owned(),
"media.class" => {
media_class = Some(match value {
"Audio/Sink" => MediaClass::Sink,
"Audio/Source" => MediaClass::Source,
_ => return None,
})
}
// alsa_input.pci-0000_66_00.6.analog-stereo
"node.name" => node_name = value.to_owned(),
// Family 17h/19h HD Audio Controller Analog Stereo
"node.description" => node_description = value,
_ => (),
}
}
let device = Node {
object_id: object_id?,
device_id,
card_profile_device,
media_class: media_class?,
description: if device_profile_description.is_empty() {
node_description.to_owned()
} else {
let device_name = node_description
.strip_suffix(device_profile_description)
.unwrap_or(node_description)
.trim_ascii_end();
device_name.to_owned()
},
device_profile_description: device_profile_description.to_owned(),
device_profile_pro,
icon_name,
audio_channels,
audio_position,
node_name,
state: match info.state() {
NodeState::Idle => State::Idle,
NodeState::Running => State::Running,
NodeState::Creating => State::Creating,
NodeState::Suspended => State::Suspended,
NodeState::Error(why) => State::Error(why.to_owned()),
},
};
Some(device)
}
}
#[derive(Clone, Debug, Default)]
pub struct NodeProps {
pub mute: Option<bool>,
pub monitor_mute: Option<bool>,
pub channel_map: Option<Vec<Channel>>,
pub channel_volumes: Option<Vec<f32>>,
}
impl NodeProps {
pub fn from_pod(pod: &Pod) -> Option<Self> {
let props = pod.as_object().ok()?;
let props = NodeProps {
mute: props
.find_prop(Id(libspa_sys::SPA_PROP_mute))
.and_then(|prop| prop.value().get_bool().ok()),
monitor_mute: props
.find_prop(Id(libspa_sys::SPA_PROP_monitorMute))
.and_then(|prop| prop.value().get_bool().ok()),
channel_map: props
.find_prop(Id(libspa_sys::SPA_PROP_channelMap))
.and_then(|prop| unsafe { array_from_pod::<Channel>(prop.value()) }),
channel_volumes: props
.find_prop(Id(libspa_sys::SPA_PROP_channelVolumes))
.and_then(|prop| unsafe { array_from_pod::<c_float>(prop.value()) }),
};
if props.mute.is_none()
&& props.monitor_mute.is_none()
&& props.channel_map.is_none()
&& props.channel_volumes.is_none()
{
None
} else {
Some(props)
}
}
pub fn merge(&mut self, other: NodeProps) {
if other.mute.is_some() {
self.mute = other.mute
}
if other.monitor_mute.is_some() {
self.monitor_mute = other.monitor_mute;
}
if other.channel_map.is_some() {
self.channel_map = other.channel_map;
}
if other.channel_volumes.is_some() {
self.channel_volumes = other.channel_volumes;
}
}
}

View file

@ -0,0 +1,98 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Currently unusued
use crate::pipewire::Direction;
use pipewire::port::PortInfoRef;
#[must_use]
#[derive(Clone, Debug)]
pub struct Port {
pub node_id: u32,
pub object_id: u32,
pub port_id: u32,
pub audio_channel: String,
pub format_dsp: String,
pub object_path: String,
pub port_direction: Direction,
pub port_group: String,
pub port_name: String,
pub port_alias: String,
pub port_physical: bool,
pub port_terminal: bool,
pub port_monitor: bool,
}
impl Port {
/// Attains process info from a pipewire info port.
#[must_use]
pub fn from_port(info: &PortInfoRef) -> Option<Self> {
let props = info.props()?;
let object_id = info.id();
let port_direction = match info.direction() {
libspa::utils::Direction::Input => Direction::Input,
libspa::utils::Direction::Output => Direction::Output,
_ => return None,
};
let mut node_id = 0;
let mut port_id = 0;
let mut port_monitor = false;
let mut port_physical = false;
let mut port_terminal = false;
let mut audio_channel = String::new();
let mut format_dsp = String::new();
let mut object_path = String::new();
let mut port_alias = String::new();
let mut port_group = String::new();
let mut port_name = String::new();
for (entry, value) in props.iter() {
match entry {
// 32 bit float mono audio
"format.dsp" => format_dsp = value.to_owned(),
// FR
"audio.channel" => audio_channel = value.to_owned(),
// playback
"port.group" => port_group = value.to_owned(),
// 1
"port.id" => port_id = value.parse::<u32>().ok()?,
// false
"port.monitor" => port_monitor = value == "true",
// true
"port.physical" => port_physical = value == "true",
// true
"port.terminal" => port_terminal = value == "true",
// alsa:acp:Device:3:playback:playback_1
"object.path" => object_path = value.to_owned(),
// playback_FR
"port.name" => port_name = value.to_owned(),
// MosArt USB Audio Device:playback_FR
"port.alias" => port_alias = value.to_owned(),
// 59
"node.id" => node_id = value.parse::<u32>().ok()?,
_ => (),
}
}
let port = Port {
format_dsp,
audio_channel,
port_id,
port_direction,
object_path,
port_name,
port_alias,
port_group,
port_monitor,
port_physical,
port_terminal,
node_id,
object_id,
};
Some(port)
}
}

View file

@ -0,0 +1,53 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{Availability, spa_utils::string_from_pod};
use libspa::pod::Pod;
#[derive(Clone, Debug)]
pub struct Profile {
pub index: i32,
pub priority: i32,
pub available: Availability,
pub name: String,
pub description: String,
}
impl Profile {
pub fn from_pod(pod: &Pod) -> Option<Self> {
let mut index = 0;
let mut priority = 0;
let mut available = Availability::Unknown;
let mut name = String::new();
let mut description = String::new();
let profile = pod.as_object().ok()?;
for prop in profile.props() {
match prop.key().0 {
libspa_sys::SPA_PARAM_PROFILE_index => index = prop.value().get_int().ok()?,
libspa_sys::SPA_PARAM_PROFILE_priority => priority = prop.value().get_int().ok()?,
libspa_sys::SPA_PARAM_PROFILE_available => {
available = match prop.value().get_id().unwrap().0 {
libspa_sys::SPA_PARAM_AVAILABILITY_no => Availability::No,
libspa_sys::SPA_PARAM_AVAILABILITY_yes => Availability::Yes,
_ => Availability::Unknown,
};
}
libspa_sys::SPA_PARAM_PROFILE_name => name = string_from_pod(prop.value())?,
libspa_sys::SPA_PARAM_PROFILE_description => {
description = string_from_pod(prop.value())?;
}
_ => (),
}
}
Some(Self {
index,
priority,
available,
name,
description,
})
}
}

View file

@ -0,0 +1,92 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use std::ffi::{c_float, c_int};
use crate::{
Availability, Channel, Direction,
spa_utils::{array_from_pod, string_from_pod},
};
use libspa::{pod::Pod, utils::Id};
#[derive(Clone, Debug, Default)]
pub struct Route {
pub index: i32,
pub priority: i32,
pub device: i32,
pub available: Availability,
pub direction: Direction,
pub name: String,
pub description: String,
pub devices: Vec<i32>,
pub props: Option<RouteProps>,
}
#[derive(Clone, Debug, Default)]
pub struct RouteProps {
pub mute: Option<bool>,
pub monitor_mute: Option<bool>,
pub channel_map: Option<Vec<Channel>>,
pub channel_volumes: Option<Vec<f32>>,
}
impl Route {
pub fn from_pod(pod: &Pod) -> Option<Self> {
let mut this = Route::default();
let route = pod.as_object().ok()?;
for prop in route.props() {
match prop.key().0 {
libspa_sys::SPA_PARAM_ROUTE_index => this.index = prop.value().get_int().ok()?,
libspa_sys::SPA_PARAM_ROUTE_priority => {
this.priority = prop.value().get_int().ok()?
}
libspa_sys::SPA_PARAM_ROUTE_device => this.device = prop.value().get_int().ok()?,
libspa_sys::SPA_PARAM_ROUTE_available => {
this.available = match prop.value().get_id().unwrap().0 {
libspa_sys::SPA_PARAM_AVAILABILITY_no => Availability::No,
libspa_sys::SPA_PARAM_AVAILABILITY_yes => Availability::Yes,
_ => Availability::Unknown,
};
}
libspa_sys::SPA_PARAM_ROUTE_name => this.name = string_from_pod(prop.value())?,
libspa_sys::SPA_PARAM_ROUTE_description => {
this.description = string_from_pod(prop.value())?;
}
libspa_sys::SPA_PARAM_ROUTE_direction => {
this.direction = match prop.value().get_id().unwrap().0 {
libspa_sys::SPA_DIRECTION_OUTPUT => Direction::Output,
_ => Direction::Input,
}
}
libspa_sys::SPA_PARAM_ROUTE_devices => {
if let Some(data) = unsafe { array_from_pod::<c_int>(prop.value()) } {
this.devices = data;
}
}
libspa_sys::SPA_PARAM_ROUTE_props => {
let props = prop.value().as_object().ok()?;
this.props = Some(RouteProps {
mute: props
.find_prop(Id(libspa_sys::SPA_PROP_mute))
.and_then(|prop| prop.value().get_bool().ok()),
monitor_mute: props
.find_prop(Id(libspa_sys::SPA_PROP_monitorMute))
.and_then(|prop| prop.value().get_bool().ok()),
channel_map: props
.find_prop(Id(libspa_sys::SPA_PROP_channelMap))
.and_then(|prop| unsafe { array_from_pod::<Channel>(prop.value()) }),
channel_volumes: props
.find_prop(Id(libspa_sys::SPA_PROP_channelVolumes))
.and_then(|prop| unsafe { array_from_pod::<c_float>(prop.value()) }),
})
}
_ => (),
}
}
Some(this)
}
}

View file

@ -0,0 +1,152 @@
// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use libspa::pod::Pod;
use std::ffi::CStr;
/// Read a `Pod`'s string if it contains a string.
pub fn string_from_pod(pod: &Pod) -> Option<String> {
if !pod.is_string() {
return None;
}
let mut cstr = std::ptr::null();
unsafe {
// SAFETY: Pod is checked to be a string beforehand
if libspa_sys::spa_pod_get_string(pod.as_raw_ptr(), &mut cstr) == 0 {
if !cstr.is_null() {
return Some(String::from_utf8_lossy(CStr::from_ptr(cstr).to_bytes()).into_owned());
}
}
}
None
}
/// SAFETY: Must be absolutely certain that the array is a compatible array.
pub unsafe fn array_from_pod<CType: Copy>(pod: &Pod) -> Option<Vec<CType>> {
if !pod.is_array() {
return None;
}
let mut len = 0;
unsafe {
let array: *mut CType = libspa_sys::spa_pod_get_array(pod.as_raw_ptr(), &mut len).cast();
if array.is_null() {
return None;
}
Some(std::slice::from_raw_parts(array, len as usize).to_vec())
}
}
#[repr(u32)]
#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq)]
pub enum Channel {
#[default]
UNKNOWN = 0, // unspecified
NA, // N/A, silent
MONO, // mono stream
FL, // front left
FR, // front right
FC, // front center
LFE, // LFE
SL, // side left
SR, // side right
FLC, // front left center
FRC, // front right center
RC, // rear center
RL, // rear left
RR, // rear right
TC, // top center
TFL, // top front left
TFC, // top front center
TFR, // top front right
TRL, // top rear left
TRC, // top rear center
TRR, // top rear right
RLC, // rear left center
RRC, // rear right center
FLW, // front left wide
FRW, // front right wide
LFE2, // LFE 2
FLH, // front left high
FCH, // front center high
FRH, // front right high
TFLC, // top front left center
TFRC, // top front right center
TSL, // top side left
TSR, // top side right
LLFE, // left LFE
RLFE, // right LFE
BC, // bottom center
BLC, // bottom left center
BRC = 37, // bottom right center
AUX0 = 4096, // aux channels
AUX1,
AUX2,
AUX3,
AUX4,
AUX5,
AUX6,
AUX7,
AUX8,
AUX9,
AUX10,
AUX11,
AUX12,
AUX13,
AUX14,
AUX15,
AUX16,
AUX17,
AUX18,
AUX19,
AUX20,
AUX21,
AUX22,
AUX23,
AUX24,
AUX25,
AUX26,
AUX27,
AUX28,
AUX29,
AUX30,
AUX31,
AUX32,
AUX33,
AUX34,
AUX35,
AUX36,
AUX37,
AUX38,
AUX39,
AUX40,
AUX41,
AUX42,
AUX43,
AUX44,
AUX45,
AUX46,
AUX47,
AUX48,
AUX49,
AUX50,
AUX51,
AUX52,
AUX53,
AUX54,
AUX55,
AUX56,
AUX57,
AUX58,
AUX59,
AUX60,
AUX61,
AUX62,
AUX63 = 4159,
}