cosmic-settings/crates/cosmic-pipewire/src/node.rs
2026-05-15 16:26:20 -06:00

192 lines
5.8 KiB
Rust

// Copyright 2025 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::Channel;
use crate::spa_utils::array_from_pod;
use libspa::pod::Pod;
use libspa::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;
}
}
}