feat(sound): use cosmic-settings-daemon's varlink API
This commit is contained in:
parent
3a77442dbc
commit
9155a1e902
19 changed files with 1211 additions and 3470 deletions
|
|
@ -1,12 +0,0 @@
|
|||
[package]
|
||||
name = "cosmic-settings-pulse-subscription"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
libpulse-binding = { version = "2.30.1" }
|
||||
rustix = { version = "1.1.3", features = ["pipe"] }
|
||||
iced_futures = { git = "https://github.com/pop-os/libcosmic" }
|
||||
futures = "0.3.32"
|
||||
log = "0.4.27"
|
||||
|
|
@ -1,744 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
// Make sure not to fail if pulse not found, and reconnect?
|
||||
// change to device shouldn't send osd?
|
||||
|
||||
use futures::SinkExt;
|
||||
use futures::executor::block_on;
|
||||
use iced_futures::{Subscription, stream};
|
||||
use libpulse_binding::callbacks::ListResult;
|
||||
use libpulse_binding::channelmap::Map;
|
||||
use libpulse_binding::context::introspect::{
|
||||
CardInfo, CardProfileInfo, Introspector, ServerInfo, SinkInfo, SourceInfo,
|
||||
};
|
||||
use libpulse_binding::context::subscribe::{Facility, InterestMaskSet, Operation};
|
||||
use libpulse_binding::context::{Context, FlagSet, State};
|
||||
use libpulse_binding::def::{PortAvailable, Retval};
|
||||
use libpulse_binding::mainloop::api::MainloopApi;
|
||||
use libpulse_binding::mainloop::events::io::IoEventInternal;
|
||||
use libpulse_binding::mainloop::standard::{IterateResult, Mainloop};
|
||||
use libpulse_binding::volume::{ChannelVolumes, Volume};
|
||||
use std::borrow::Cow;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::convert::Infallible;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
|
||||
use std::os::raw::c_void;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use std::sync::mpsc;
|
||||
|
||||
pub fn subscription() -> iced_futures::Subscription<Event> {
|
||||
Subscription::run_with("pulse", |_| {
|
||||
stream::channel(20, |sender| async {
|
||||
std::thread::spawn(move || thread(sender));
|
||||
futures::future::pending().await
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn thread(sender: futures::channel::mpsc::Sender<Event>) {
|
||||
let Some(mut main_loop) = Mainloop::new() else {
|
||||
log::error!("Failed to create PA main loop");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(mut context) = Context::new(&main_loop, "cosmic-osd") else {
|
||||
log::error!("Failed to create PA context");
|
||||
return;
|
||||
};
|
||||
|
||||
let data = Rc::new(Data {
|
||||
main_loop: RefCell::new(Mainloop {
|
||||
_inner: Rc::clone(&main_loop._inner),
|
||||
}),
|
||||
introspector: context.introspect(),
|
||||
sink_volume: Cell::new(None),
|
||||
sink_mute: Cell::new(None),
|
||||
source_volume: Cell::new(None),
|
||||
source_mute: Cell::new(None),
|
||||
default_sink_name: RefCell::new(None),
|
||||
default_source_name: RefCell::new(None),
|
||||
sender: RefCell::new(sender.clone()),
|
||||
});
|
||||
|
||||
let data_clone = data.clone();
|
||||
context.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
|
||||
data_clone.subscribe_cb(facility.unwrap(), operation, index);
|
||||
})));
|
||||
|
||||
let _ = context.connect(None, FlagSet::NOFAIL, None);
|
||||
|
||||
loop {
|
||||
if sender.is_closed() {
|
||||
return;
|
||||
}
|
||||
|
||||
match main_loop.iterate(false) {
|
||||
IterateResult::Success(_) => {}
|
||||
IterateResult::Err(_e) => {
|
||||
return;
|
||||
}
|
||||
IterateResult::Quit(_e) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if context.get_state() == State::Ready {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect all available cards on startup
|
||||
data.introspector.get_card_info_list({
|
||||
let data_weak = Rc::downgrade(&data);
|
||||
move |card_info_res| {
|
||||
if let Some(data) = data_weak.upgrade() {
|
||||
data.card_info_cb(card_info_res)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
data.get_server_info();
|
||||
context.subscribe(
|
||||
InterestMaskSet::SERVER | InterestMaskSet::SINK | InterestMaskSet::SOURCE,
|
||||
|_| {},
|
||||
);
|
||||
|
||||
if let Err((err, retval)) = main_loop.run() {
|
||||
log::error!("PA main loop returned {:?}, error {}", retval, err);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Balance(Option<f32>),
|
||||
CardInfo(Card),
|
||||
DefaultSink(String),
|
||||
DefaultSource(String),
|
||||
SinkVolume(u32),
|
||||
Channels(PulseChannels),
|
||||
SinkMute(bool),
|
||||
SourceVolume(u32),
|
||||
SourceMute(bool),
|
||||
}
|
||||
|
||||
enum Request {
|
||||
Volume(u32, f32),
|
||||
Balance(u32, f32),
|
||||
Quit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PulseChannels {
|
||||
tx: mpsc::Sender<Request>,
|
||||
pipe_tx: std::fs::File,
|
||||
index: u32,
|
||||
}
|
||||
|
||||
impl Clone for PulseChannels {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
tx: self.tx.clone(),
|
||||
pipe_tx: self
|
||||
.pipe_tx
|
||||
.try_clone()
|
||||
.expect("failed to clone PulseChannels pipe writer"),
|
||||
index: self.index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Data used by the [`handle_balance_io_new`] callback.
|
||||
struct HandleBalanceData(
|
||||
Context,
|
||||
ChannelVolumes,
|
||||
Map,
|
||||
std::sync::mpsc::Receiver<Request>,
|
||||
);
|
||||
|
||||
/// Callback for creating an IO event source [`MainloopApi::io_new`].
|
||||
extern "C" fn handle_balance_io_new(
|
||||
api: *const MainloopApi,
|
||||
event: *mut IoEventInternal,
|
||||
reader_fd: RawFd,
|
||||
_flags: libpulse_binding::mainloop::events::io::FlagSet,
|
||||
data: *mut c_void,
|
||||
) {
|
||||
// Take ownership of the data and borrow its contents.
|
||||
let mut data = unsafe { Box::<HandleBalanceData>::from_raw(data as _) };
|
||||
let HandleBalanceData(ctx, volumes, map, rx) = data.as_mut();
|
||||
|
||||
// Return early if the context is not ready, and give the data back.
|
||||
if ctx.get_state() != State::Ready {
|
||||
let _ = Box::leak(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the first byte cannot be read, destroy this event source with its reader and data.
|
||||
let mut buf = [0u8; 1];
|
||||
let mut reader = unsafe { std::fs::File::from_raw_fd(reader_fd) };
|
||||
if reader.read_exact(&mut buf).is_err() {
|
||||
(unsafe { &*api })
|
||||
.io_free
|
||||
.as_ref()
|
||||
.expect("io_free function is missing")(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Give ownership of the reader back.
|
||||
_ = reader.into_raw_fd();
|
||||
|
||||
while let Ok(req) = rx.try_recv() {
|
||||
match req {
|
||||
Request::Volume(index, volume_scale) => {
|
||||
let mut intro = ctx.introspect();
|
||||
|
||||
let new_scale = Volume((volume_scale * Volume::NORMAL.0 as f32).round() as u32);
|
||||
|
||||
if let Some(v) = volumes.scale(new_scale) {
|
||||
_ = intro.set_sink_volume_by_index(
|
||||
index,
|
||||
v,
|
||||
Some(Box::new(|success| {
|
||||
if !success {
|
||||
log::error!("Failed to set sink balance");
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
Request::Balance(index, new_balance) => {
|
||||
if map.can_balance() {
|
||||
if let Some(v) = volumes.set_balance(&map, new_balance) {
|
||||
let mut intro = ctx.introspect();
|
||||
|
||||
_ = intro.set_sink_volume_by_index(
|
||||
index,
|
||||
v,
|
||||
Some(Box::new(|success| {
|
||||
if !success {
|
||||
log::error!("Failed to set sink balance");
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Request::Quit => unsafe { &*api }
|
||||
.quit
|
||||
.as_ref()
|
||||
.expect("quit function missing")(api, 0),
|
||||
}
|
||||
}
|
||||
|
||||
let _ = Box::leak(data);
|
||||
}
|
||||
|
||||
impl PulseChannels {
|
||||
fn new(
|
||||
volumes: ChannelVolumes,
|
||||
map: Map,
|
||||
api: &MainloopApi,
|
||||
index: u32,
|
||||
ctx: Context,
|
||||
) -> PulseChannels {
|
||||
let (reader, writer) = rustix::pipe::pipe_with(rustix::pipe::PipeFlags::CLOEXEC)
|
||||
.expect("failed to crate pipe");
|
||||
|
||||
let (tx, rx) = mpsc::channel::<Request>();
|
||||
|
||||
// Create IO event source object for handling speaker balance.
|
||||
let event_source = api.io_new.as_ref().unwrap()(
|
||||
api as *const _,
|
||||
reader.into_raw_fd(),
|
||||
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
|
||||
Some(handle_balance_io_new),
|
||||
Box::into_raw(Box::new(HandleBalanceData(ctx, volumes, map, rx))) as *mut c_void,
|
||||
);
|
||||
|
||||
if let Some(enable) = api.io_enable.as_ref() {
|
||||
enable(
|
||||
event_source,
|
||||
libpulse_binding::mainloop::events::io::FlagSet::INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
tx,
|
||||
pipe_tx: std::fs::File::from(writer),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the active index.
|
||||
#[inline]
|
||||
pub fn set_index(&mut self, index: u32) {
|
||||
self.index = index;
|
||||
}
|
||||
|
||||
/// Set the speaker balance of the active sink.
|
||||
pub fn set_balance(&mut self, balance: f32) {
|
||||
if let Err(err) = self.tx.send(Request::Balance(self.index, balance)) {
|
||||
log::error!("Failed to send new balance to channel");
|
||||
} else {
|
||||
self.pipe_tx
|
||||
.write_all(&[1])
|
||||
.expect("PulseChannels pipe write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the volume of the active sink.
|
||||
pub fn set_volume(&mut self, volume: f32) {
|
||||
if let Err(err) = self.tx.send(Request::Volume(self.index, volume)) {
|
||||
log::error!("Failed to send new volume to channel");
|
||||
} else {
|
||||
self.pipe_tx
|
||||
.write_all(&[1])
|
||||
.expect("PulseChannels pipe write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the pulse thread to quit.
|
||||
pub fn quit(mut self) {
|
||||
_ = self.tx.send(Request::Quit);
|
||||
_ = self.pipe_tx.write_all(&[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct Card {
|
||||
pub object_id: u32,
|
||||
pub name: String,
|
||||
pub product_name: String,
|
||||
pub variant: DeviceVariant,
|
||||
pub ports: Vec<CardPort>,
|
||||
pub profiles: Vec<CardProfile>,
|
||||
pub active_profile: Option<CardProfile>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct CardPort {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub direction: Direction,
|
||||
pub port_type: PortType,
|
||||
pub profile_port: u32,
|
||||
pub priority: u32,
|
||||
pub profiles: Vec<CardProfile>,
|
||||
pub availability: Availability,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum Availability {
|
||||
Unknown,
|
||||
No,
|
||||
Yes,
|
||||
}
|
||||
|
||||
impl From<PortAvailable> for Availability {
|
||||
fn from(pa: PortAvailable) -> Self {
|
||||
match pa {
|
||||
PortAvailable::Unknown => Availability::Unknown,
|
||||
PortAvailable::No => Availability::No,
|
||||
PortAvailable::Yes => Availability::Yes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct CardProfile {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub available: bool,
|
||||
pub n_sinks: u32,
|
||||
pub n_sources: u32,
|
||||
pub priority: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum DeviceVariant {
|
||||
Alsa { alsa_card: u32 },
|
||||
Bluez5 { address: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum Direction {
|
||||
Input,
|
||||
Output,
|
||||
Both,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub enum PortType {
|
||||
Mic,
|
||||
Speaker,
|
||||
Headphones,
|
||||
Headset,
|
||||
Digital,
|
||||
#[default]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl FromStr for PortType {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"mic" => Ok(PortType::Mic),
|
||||
"speaker" => Ok(PortType::Speaker),
|
||||
"headphones" => Ok(PortType::Headphones),
|
||||
"headset" => Ok(PortType::Headset),
|
||||
"digital" => Ok(PortType::Digital),
|
||||
_ => Ok(PortType::Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Data {
|
||||
main_loop: RefCell<Mainloop>,
|
||||
default_sink_name: RefCell<Option<String>>,
|
||||
default_source_name: RefCell<Option<String>>,
|
||||
sink_volume: Cell<Option<u32>>,
|
||||
sink_mute: Cell<Option<bool>>,
|
||||
source_volume: Cell<Option<u32>>,
|
||||
source_mute: Cell<Option<bool>>,
|
||||
introspector: Introspector,
|
||||
sender: RefCell<futures::channel::mpsc::Sender<Event>>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
fn card_info_cb(self: &Rc<Self>, card_info: ListResult<&CardInfo>) {
|
||||
if let ListResult::Item(card_info) = card_info {
|
||||
let Some(object_id) = card_info
|
||||
.proplist
|
||||
.get_str("object.id")
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let variant = if let Some(alsa_card) = card_info
|
||||
.proplist
|
||||
.get_str("alsa.card")
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
{
|
||||
DeviceVariant::Alsa { alsa_card }
|
||||
} else if let Some(address) = card_info.proplist.get_str("api.bluez5.address") {
|
||||
DeviceVariant::Bluez5 { address }
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let card = Card {
|
||||
name: card_info
|
||||
.name
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
product_name: card_info
|
||||
.proplist
|
||||
.get_str("device.product.name")
|
||||
.unwrap_or_default(),
|
||||
object_id,
|
||||
variant,
|
||||
ports: card_info
|
||||
.ports
|
||||
.iter()
|
||||
.map(|port| CardPort {
|
||||
name: port.name.as_ref().map(Cow::to_string).unwrap_or_default(),
|
||||
description: port
|
||||
.description
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
direction: match port.direction.bits() {
|
||||
x if x == libpulse_binding::direction::FlagSet::INPUT.bits() => {
|
||||
Direction::Input
|
||||
}
|
||||
x if x == libpulse_binding::direction::FlagSet::OUTPUT.bits() => {
|
||||
Direction::Output
|
||||
}
|
||||
_ => Direction::Both,
|
||||
},
|
||||
port_type: port
|
||||
.proplist
|
||||
.get_str("port.type")
|
||||
.as_deref()
|
||||
.map(|s| PortType::from_str(s).unwrap())
|
||||
.unwrap_or_default(),
|
||||
profile_port: port
|
||||
.proplist
|
||||
.get_str("card.profile.port")
|
||||
.and_then(|v| v.parse::<u32>().ok())
|
||||
.unwrap_or(0),
|
||||
priority: port.priority,
|
||||
profiles: collect_profiles(&port.profiles),
|
||||
availability: port.available.into(),
|
||||
})
|
||||
.collect(),
|
||||
profiles: collect_profiles(&card_info.profiles),
|
||||
active_profile: card_info.active_profile.as_deref().map(CardProfile::from),
|
||||
};
|
||||
|
||||
if block_on(self.sender.borrow_mut().send(Event::CardInfo(card))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn server_info_cb(self: &Rc<Self>, server_info: &ServerInfo) {
|
||||
let new_default_sink_name = server_info
|
||||
.default_sink_name
|
||||
.as_ref()
|
||||
.map(|x| x.clone().into_owned());
|
||||
let mut default_sink_name = self.default_sink_name.borrow_mut();
|
||||
if new_default_sink_name != *default_sink_name {
|
||||
if let Some(name) = &new_default_sink_name {
|
||||
_ = block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::DefaultSink(name.clone())),
|
||||
);
|
||||
self.get_sink_info_by_name(name);
|
||||
}
|
||||
*default_sink_name = new_default_sink_name;
|
||||
}
|
||||
|
||||
let new_default_source_name = server_info
|
||||
.default_source_name
|
||||
.as_ref()
|
||||
.map(|x| x.clone().into_owned());
|
||||
let mut default_source_name = self.default_source_name.borrow_mut();
|
||||
if new_default_source_name != *default_source_name {
|
||||
if let Some(name) = &new_default_source_name {
|
||||
_ = block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::DefaultSource(name.clone())),
|
||||
);
|
||||
self.get_source_info_by_name(name);
|
||||
}
|
||||
*default_source_name = new_default_source_name;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_server_info(self: &Rc<Self>) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_server_info(move |server_info| data.server_info_cb(server_info));
|
||||
}
|
||||
|
||||
fn sink_info_cb(&self, sink_info_res: ListResult<&SinkInfo>) {
|
||||
if let ListResult::Item(sink_info) = sink_info_res {
|
||||
if sink_info.name.as_deref() != self.default_sink_name.borrow().as_deref() {
|
||||
return;
|
||||
}
|
||||
let balance = (sink_info.channel_map.can_balance()
|
||||
&& sink_info.base_volume.is_normal())
|
||||
.then(|| sink_info.volume.get_balance(&sink_info.channel_map));
|
||||
|
||||
let volume = sink_info.volume.max().0 / (Volume::NORMAL.0 / 100);
|
||||
if self.sink_mute.get() != Some(sink_info.mute) {
|
||||
self.sink_mute.set(Some(sink_info.mute));
|
||||
if block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::SinkMute(sink_info.mute)),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
if self.sink_volume.get() != Some(volume) {
|
||||
self.sink_volume.set(Some(volume));
|
||||
if block_on(self.sender.borrow_mut().send(Event::SinkVolume(volume))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
if block_on(self.sender.borrow_mut().send(Event::Balance(balance))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
let mut main_loop = self.main_loop.borrow_mut();
|
||||
let api = main_loop.get_api();
|
||||
if let Some(mut ctx) = Context::new(&*main_loop, "balance") {
|
||||
let _ = ctx.connect(None, FlagSet::NOFAIL, None);
|
||||
|
||||
let channels = PulseChannels::new(
|
||||
sink_info.volume,
|
||||
sink_info.channel_map,
|
||||
api,
|
||||
sink_info.index,
|
||||
ctx,
|
||||
);
|
||||
|
||||
if block_on(self.sender.borrow_mut().send(Event::Channels(channels))).is_err() {
|
||||
main_loop.quit(Retval(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn source_info_cb(&self, source_info_res: ListResult<&SourceInfo>) {
|
||||
if let ListResult::Item(source_info) = source_info_res {
|
||||
if source_info.name.as_deref() != self.default_source_name.borrow().as_deref() {
|
||||
return;
|
||||
}
|
||||
let volume = source_info.volume.max().0 / (Volume::NORMAL.0 / 100);
|
||||
if self.source_mute.get() != Some(source_info.mute) {
|
||||
self.source_mute.set(Some(source_info.mute));
|
||||
if block_on(
|
||||
self.sender
|
||||
.borrow_mut()
|
||||
.send(Event::SourceMute(source_info.mute)),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
if self.source_volume.get() != Some(volume) {
|
||||
self.source_volume.set(Some(volume));
|
||||
if block_on(self.sender.borrow_mut().send(Event::SourceVolume(volume))).is_err() {
|
||||
self.main_loop.borrow_mut().quit(Retval(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_card_info_by_index(self: &Rc<Self>, index: u32) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_card_info_by_index(index, move |card_info_res| {
|
||||
data.card_info_cb(card_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_sink_info_by_index(self: &Rc<Self>, index: u32) {
|
||||
let data = self.clone();
|
||||
self.introspector.get_sink_info_by_index(
|
||||
index,
|
||||
move |sink_info_res: ListResult<&SinkInfo<'_>>| {
|
||||
if let ListResult::Item(ref info) = sink_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.sink_info_cb(sink_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_sink_info_by_name(self: &Rc<Self>, name: &str) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_sink_info_by_name(name, move |sink_info_res| {
|
||||
if let ListResult::Item(ref info) = sink_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.sink_info_cb(sink_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_source_info_by_index(self: &Rc<Self>, index: u32) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_source_info_by_index(index, move |source_info_res| {
|
||||
if let ListResult::Item(ref info) = source_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.source_info_cb(source_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_source_info_by_name(self: &Rc<Self>, name: &str) {
|
||||
let data = self.clone();
|
||||
self.introspector
|
||||
.get_source_info_by_name(name, move |source_info_res| {
|
||||
if let ListResult::Item(ref info) = source_info_res {
|
||||
if let Some(card_index) = info.card {
|
||||
let data_clone = data.clone();
|
||||
data.introspector.get_card_info_by_index(
|
||||
card_index,
|
||||
move |card_info_res| {
|
||||
data_clone.card_info_cb(card_info_res);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
data.source_info_cb(source_info_res);
|
||||
});
|
||||
}
|
||||
|
||||
fn subscribe_cb(
|
||||
self: &Rc<Self>,
|
||||
facility: Facility,
|
||||
_operation: Option<Operation>,
|
||||
index: u32,
|
||||
) {
|
||||
match facility {
|
||||
Facility::Server => {
|
||||
self.get_server_info();
|
||||
}
|
||||
Facility::Sink => {
|
||||
self.get_sink_info_by_index(index);
|
||||
}
|
||||
Facility::Source => {
|
||||
self.get_source_info_by_index(index);
|
||||
}
|
||||
Facility::Card => {
|
||||
self.get_card_info_by_index(index);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_profiles(profiles: &[CardProfileInfo]) -> Vec<CardProfile> {
|
||||
profiles.iter().map(CardProfile::from).collect()
|
||||
}
|
||||
|
||||
impl From<&CardProfileInfo<'_>> for CardProfile {
|
||||
fn from(profile: &CardProfileInfo) -> Self {
|
||||
CardProfile {
|
||||
name: profile
|
||||
.name
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
description: profile
|
||||
.description
|
||||
.as_ref()
|
||||
.map(Cow::to_string)
|
||||
.unwrap_or_default(),
|
||||
available: profile.available,
|
||||
n_sinks: profile.n_sinks,
|
||||
n_sources: profile.n_sources,
|
||||
priority: profile.priority,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
[package]
|
||||
name = "cosmic-settings-sound-subscription"
|
||||
version = "1.0.7"
|
||||
edition = "2024"
|
||||
rust-version.workspace = true
|
||||
license = "MPL-2.0"
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
cosmic-pipewire = { path = "../../crates/cosmic-pipewire" }
|
||||
futures = "0.3.32"
|
||||
intmap = "3.1.3"
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false }
|
||||
numtoa = "1.0.0-alpha1"
|
||||
rustix = "1.1.3"
|
||||
tokio = { version = "1.49.0", features = ["process", "rt", "time"] }
|
||||
tracing = { version = "0.1.44", default-features = false }
|
||||
|
||||
[features]
|
||||
# Set profile on first load
|
||||
auto-profile-init = []
|
||||
|
|
@ -1,359 +0,0 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
## 1. Definitions
|
||||
|
||||
### 1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
### 1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
### 1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
### 1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
### 1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
+ (a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
+ (b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
### 1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
### 1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
### 1.8. "License"
|
||||
means this document.
|
||||
|
||||
### 1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
### 1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
+ (a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
+ (b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
### 1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
### 1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
### 1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
### 1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
## 2. License Grants and Conditions
|
||||
|
||||
### 2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
+ (a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
+ (b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
### 2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
### 2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
+ (a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
+ (b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
+ (c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
### 2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
### 2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
### 2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
### 2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
## 3. Responsibilities
|
||||
|
||||
### 3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
### 3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
+ (a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
+ (b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
### 3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
### 3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
### 3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
## 4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
## 5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
|
||||
## 6. Disclaimer of Warranty
|
||||
|
||||
**Covered Software is provided under this License on an "as is"
|
||||
basis, without warranty of any kind, either expressed, implied, or
|
||||
statutory, including, without limitation, warranties that the
|
||||
Covered Software is free of defects, merchantable, fit for a
|
||||
particular purpose or non-infringing. The entire risk as to the
|
||||
quality and performance of the Covered Software is with You.
|
||||
Should any Covered Software prove defective in any respect, You
|
||||
(not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an
|
||||
essential part of this License. No use of any Covered Software is
|
||||
authorized under this License except under this disclaimer.**
|
||||
|
||||
|
||||
#7. Limitation of Liability
|
||||
|
||||
**Under no circumstances and under no legal theory, whether tort
|
||||
(including negligence), contract, or otherwise, shall any
|
||||
Contributor, or anyone who distributes Covered Software as
|
||||
permitted above, be liable to You for any direct, indirect,
|
||||
special, incidental, or consequential damages of any character
|
||||
including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any
|
||||
and all other commercial damages or losses, even if such party
|
||||
shall have been informed of the possibility of such damages. This
|
||||
limitation of liability shall not apply to liability for death or
|
||||
personal injury resulting from such party's negligence to the
|
||||
extent applicable law prohibits such limitation. Some
|
||||
jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and
|
||||
limitation may not apply to You.**
|
||||
|
||||
|
||||
## 8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
## 9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
## 10. Versions of the License
|
||||
|
||||
### 10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
### 10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
### 10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
### 10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
|
||||
|
|
@ -1,956 +0,0 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use cosmic::Task;
|
||||
use cosmic::iced::stream;
|
||||
use cosmic_pipewire as pipewire;
|
||||
use futures::{SinkExt, Stream};
|
||||
use intmap::IntMap;
|
||||
use pipewire::Availability;
|
||||
use std::process::Stdio;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
pub type DeviceId = u32;
|
||||
pub type NodeId = u32;
|
||||
pub type ProfileId = i32;
|
||||
pub type RouteId = u32;
|
||||
|
||||
pub fn watch() -> impl Stream<Item = Message> + Send + 'static {
|
||||
stream::channel(
|
||||
1,
|
||||
|mut emitter: futures::channel::mpsc::Sender<Message>| async move {
|
||||
loop {
|
||||
let (cancel_tx, cancel_rx) = futures::channel::oneshot::channel::<()>();
|
||||
let sender = Arc::new((Mutex::new(Vec::new()), tokio::sync::Notify::const_new()));
|
||||
let receiver = sender.clone();
|
||||
|
||||
_ = emitter
|
||||
.send(Message::SubHandle(Arc::new(SubscriptionHandle {
|
||||
cancel_tx,
|
||||
pipewire: pipewire::run(move |event| {
|
||||
sender.0.lock().unwrap().push(event);
|
||||
sender.1.notify_one();
|
||||
}),
|
||||
})))
|
||||
.await;
|
||||
|
||||
let forwarder = Box::pin(async {
|
||||
loop {
|
||||
_ = receiver.1.notified().await;
|
||||
let events = std::mem::take(&mut *receiver.0.lock().unwrap());
|
||||
if !events.is_empty() {
|
||||
_ = emitter.send(Message::Server(Arc::from(events))).await;
|
||||
tokio::time::sleep(Duration::from_millis(64)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
futures::future::select(cancel_rx, forwarder).await;
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Model {
|
||||
subscription_handle: Option<SubscriptionHandle>,
|
||||
|
||||
pub device_profile_dropdowns: Vec<(DeviceId, String, Option<usize>, Vec<u32>, Vec<String>)>,
|
||||
|
||||
// Translated text
|
||||
pub unplugged_text: String,
|
||||
pub hd_audio_text: String,
|
||||
pub usb_audio_text: String,
|
||||
|
||||
device_ids: IntMap<NodeId, DeviceId>,
|
||||
node_names: IntMap<NodeId, String>,
|
||||
card_profile_devices: IntMap<NodeId, u32>,
|
||||
node_route_indexes: IntMap<NodeId, i32>,
|
||||
|
||||
device_names: IntMap<DeviceId, String>,
|
||||
device_profiles: IntMap<DeviceId, Vec<pipewire::Profile>>,
|
||||
active_profiles: IntMap<DeviceId, pipewire::Profile>,
|
||||
device_routes: IntMap<DeviceId, Vec<pipewire::Route>>,
|
||||
|
||||
/** Sink devices */
|
||||
|
||||
/// Description of a sink device and its port
|
||||
sinks: Vec<String>,
|
||||
/// Node IDs for sinks
|
||||
sink_node_ids: Vec<NodeId>,
|
||||
/// Index of active sink device.
|
||||
active_sink: Option<usize>,
|
||||
/// Node ID of active sink device.
|
||||
active_sink_node: Option<NodeId>,
|
||||
/// Device ID of active sink device.
|
||||
active_sink_device: Option<DeviceId>,
|
||||
/// Device identifier of the default sink.
|
||||
active_sink_node_name: String,
|
||||
|
||||
/** Source devices */
|
||||
|
||||
/// Product names for source devices.
|
||||
sources: Vec<String>,
|
||||
/// Node IDs for sources
|
||||
source_node_ids: Vec<NodeId>,
|
||||
/// Index of active source device.
|
||||
active_source: Option<usize>,
|
||||
/// Node ID of active source device.
|
||||
active_source_node: Option<NodeId>,
|
||||
/// Device ID of active source device.
|
||||
active_source_device: Option<DeviceId>,
|
||||
/// Node identifier of the default source.
|
||||
active_source_node_name: String,
|
||||
|
||||
changing_sink_device: Option<DeviceId>,
|
||||
changing_source_device: Option<DeviceId>,
|
||||
|
||||
pub sink_volume_text: String,
|
||||
pub source_volume_text: String,
|
||||
pub sink_balance: Option<f32>,
|
||||
|
||||
pub sink_volume: u32,
|
||||
pub source_volume: u32,
|
||||
|
||||
pub sink_mute: bool,
|
||||
sink_volume_debounce: bool,
|
||||
pub source_mute: bool,
|
||||
source_volume_debounce: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn active_sink(&self) -> Option<usize> {
|
||||
self.active_sink
|
||||
}
|
||||
|
||||
pub fn active_source(&self) -> Option<usize> {
|
||||
self.active_source
|
||||
}
|
||||
|
||||
pub fn sinks(&self) -> &[String] {
|
||||
&self.sinks
|
||||
}
|
||||
|
||||
pub fn sources(&self) -> &[String] {
|
||||
&self.sources
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
if let Some(handle) = self.subscription_handle.take() {
|
||||
_ = handle.cancel_tx.send(());
|
||||
_ = handle.pipewire.send(pipewire::Request::Quit);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to the pipewire-rs thread.
|
||||
pub fn pipewire_send(&self, request: pipewire::Request) {
|
||||
if let Some(handle) = self.subscription_handle.as_ref() {
|
||||
_ = handle.pipewire.send(request);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets and applies a profile to a device with wpctl.
|
||||
///
|
||||
/// Requires using the device ID rather than a node ID.
|
||||
pub fn set_profile(&mut self, device_id: DeviceId, index: u32, save: bool) {
|
||||
if save {
|
||||
self.changing_sink_device = self
|
||||
.device_ids
|
||||
.iter()
|
||||
.find(|(node_id, _device)| self.active_sink_node == Some(*node_id))
|
||||
.and_then(|(_node_id, &device)| {
|
||||
if device == device_id {
|
||||
Some(device_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
self.changing_source_device = self
|
||||
.device_ids
|
||||
.iter()
|
||||
.find(|(node_id, _device)| self.active_source_node == Some(*node_id))
|
||||
.and_then(|(_node_id, &device)| {
|
||||
if device == device_id {
|
||||
Some(device_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut update = false;
|
||||
|
||||
if let Some(profiles) = self.device_profiles.get(device_id) {
|
||||
for profile in profiles {
|
||||
if profile.index as u32 == index {
|
||||
self.active_profiles.insert(device_id, profile.clone());
|
||||
self.pipewire_send(pipewire::Request::SetProfile(device_id, index, save));
|
||||
update = true;
|
||||
}
|
||||
}
|
||||
|
||||
if update {
|
||||
self.update_ui_profiles();
|
||||
}
|
||||
|
||||
// Use pw-cli as a fallback in case it wasn't set correctly.
|
||||
tokio::spawn(async move {
|
||||
set_profile(device_id, index, save).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the balance of channel volumes on the sink device.
|
||||
pub fn set_sink_balance(&mut self, balance: u32) -> Task<Message> {
|
||||
self.sink_balance = (balance != 100).then(|| balance as f32 / 100.);
|
||||
if self.sink_volume_debounce {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
if let Some(id) = self.active_sink_node {
|
||||
self.sink_volume_debounce = true;
|
||||
return cosmic::Task::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(128)).await;
|
||||
Message::SinkVolumeApply(id)
|
||||
});
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Change the default sink device
|
||||
pub fn set_default_sink(&mut self, pos: usize) -> Task<Message> {
|
||||
if let Some(&node_id) = self.sink_node_ids.get(pos) {
|
||||
self.set_default_sink_node_id(node_id);
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub fn set_default_sink_node_id(&mut self, node_id: NodeId) {
|
||||
tracing::debug!(target: "sound", "set default sink node {node_id}");
|
||||
self.set_default_sink_id(node_id);
|
||||
|
||||
// Use pactl if the node is not a device node.
|
||||
let virtual_sink_name: Option<String> =
|
||||
if let Some(device) = self.device_ids.get(node_id).cloned() {
|
||||
// Get route index of the selected node and apply it to the device.
|
||||
if let Some((card_profile_device, route_index)) = self
|
||||
.card_profile_devices
|
||||
.get(node_id)
|
||||
.cloned()
|
||||
.zip(self.node_route_indexes.get(node_id).cloned())
|
||||
{
|
||||
self.pipewire_send(pipewire::Request::SetRoute(
|
||||
device,
|
||||
card_profile_device,
|
||||
route_index as u32,
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
} else {
|
||||
self.node_names.get(node_id).cloned()
|
||||
};
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
if let Some(node_name) = virtual_sink_name {
|
||||
pactl_set_default_sink(&node_name).await
|
||||
} else {
|
||||
set_default(node_id).await
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Toggle the mute property of the sink device.
|
||||
pub fn toggle_sink_mute(&mut self) {
|
||||
self.sink_mute = !self.sink_mute;
|
||||
if let Some(node_id) = self.active_sink_node {
|
||||
let mute = self.sink_mute;
|
||||
if let Some(handle) = self.subscription_handle.as_mut() {
|
||||
_ = handle
|
||||
.pipewire
|
||||
.send(pipewire::Request::SetNodeMute(node_id, mute));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the sink device's volume.
|
||||
pub fn set_sink_volume(&mut self, volume: u32) -> Task<Message> {
|
||||
self.sink_volume = volume;
|
||||
self.sink_volume_text = numtoa::BaseN::<10>::u32(volume).as_str().to_owned();
|
||||
if self.sink_volume_debounce {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Wait for the debounce duration before applying the volume change.
|
||||
if let Some(node_id) = self.active_sink_node {
|
||||
self.sink_volume_debounce = true;
|
||||
return cosmic::Task::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(128)).await;
|
||||
Message::SinkVolumeApply(node_id)
|
||||
});
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Change the default source device.
|
||||
pub fn set_default_source(&mut self, pos: usize) -> Task<Message> {
|
||||
if let Some(&node_id) = self.source_node_ids.get(pos) {
|
||||
self.set_default_source_node_id(node_id);
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub fn set_default_source_node_id(&mut self, node_id: NodeId) {
|
||||
tracing::debug!(target: "sound", "set default source node {node_id}");
|
||||
self.set_default_source_id(node_id);
|
||||
|
||||
// Use pactl if the node is not a device node.
|
||||
let virtual_source_name: Option<String> =
|
||||
if let Some(device) = self.device_ids.get(node_id).cloned() {
|
||||
// Get route index of the selected node and apply it to the device.
|
||||
if let Some((card_profile_device, route_index)) = self
|
||||
.card_profile_devices
|
||||
.get(node_id)
|
||||
.cloned()
|
||||
.zip(self.node_route_indexes.get(node_id).cloned())
|
||||
{
|
||||
self.pipewire_send(pipewire::Request::SetRoute(
|
||||
device,
|
||||
card_profile_device,
|
||||
route_index as u32,
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
} else {
|
||||
self.node_names.get(node_id).cloned()
|
||||
};
|
||||
|
||||
tokio::task::spawn(async move {
|
||||
if let Some(node_name) = virtual_source_name {
|
||||
pactl_set_default_source(&node_name).await
|
||||
} else {
|
||||
set_default(node_id).await
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Toggle the mute property of the source device.
|
||||
pub fn toggle_source_mute(&mut self) {
|
||||
self.source_mute = !self.source_mute;
|
||||
if let Some(node_id) = self.active_source_node {
|
||||
let mute = self.source_mute;
|
||||
if let Some(handle) = self.subscription_handle.as_mut() {
|
||||
_ = handle
|
||||
.pipewire
|
||||
.send(pipewire::Request::SetNodeMute(node_id, mute));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Change the source device's volume.
|
||||
pub fn set_source_volume(&mut self, volume: u32) -> Task<Message> {
|
||||
self.source_volume = volume;
|
||||
self.source_volume_text = numtoa::BaseN::<10>::u32(volume).as_str().to_owned();
|
||||
if self.source_volume_debounce {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Wait for the debounce duration before applying the volume change.
|
||||
if let Some(node_id) = self.active_source_node {
|
||||
self.source_volume_debounce = true;
|
||||
return cosmic::Task::future(async move {
|
||||
tokio::time::sleep(Duration::from_millis(128)).await;
|
||||
Message::SourceVolumeApply(node_id)
|
||||
});
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Server(events) => {
|
||||
Arc::into_inner(events)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.for_each(|event| self.pipewire_update(event));
|
||||
}
|
||||
|
||||
Message::SinkVolumeApply(node_id) => {
|
||||
self.sink_volume_debounce = false;
|
||||
self.pipewire_send(pipewire::Request::SetNodeVolume(
|
||||
node_id,
|
||||
self.sink_volume as f32 / 100.0,
|
||||
self.sink_balance,
|
||||
));
|
||||
}
|
||||
|
||||
Message::SourceVolumeApply(node_id) => {
|
||||
self.source_volume_debounce = false;
|
||||
self.pipewire_send(pipewire::Request::SetNodeVolume(
|
||||
node_id,
|
||||
self.source_volume as f32 / 100.0,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
Message::SubHandle(handle) => {
|
||||
if let Some(handle) = Arc::into_inner(handle) {
|
||||
self.subscription_handle = Some(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn pipewire_update(&mut self, event: pipewire::Event) {
|
||||
match event {
|
||||
pipewire::Event::NodeProperties(id, props) => {
|
||||
if self.active_sink_node == Some(id) {
|
||||
if self.sink_volume_debounce {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mute) = props.mute {
|
||||
self.sink_mute = mute;
|
||||
}
|
||||
|
||||
if let Some(channel_volumes) = props.channel_volumes {
|
||||
let (volume, balance) =
|
||||
pipewire::volume::from_channel_volumes(&channel_volumes);
|
||||
|
||||
self.sink_balance = balance;
|
||||
self.sink_volume = (volume * 100.0) as u32;
|
||||
self.sink_volume_text = numtoa::BaseN::<10>::u32(self.sink_volume)
|
||||
.as_str()
|
||||
.to_owned();
|
||||
}
|
||||
} else if self.active_source_node == Some(id) {
|
||||
if self.source_volume_debounce {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mute) = props.mute {
|
||||
self.source_mute = mute;
|
||||
}
|
||||
|
||||
if let Some(channel_volumes) = props.channel_volumes {
|
||||
let (volume, _balance) =
|
||||
pipewire::volume::from_channel_volumes(&channel_volumes);
|
||||
self.source_volume = (volume * 100.0) as u32;
|
||||
self.source_volume_text = numtoa::BaseN::<10>::u32(self.source_volume)
|
||||
.as_str()
|
||||
.to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::Event::ActiveProfile(id, profile) => {
|
||||
tracing::debug!(
|
||||
target: "sound",
|
||||
"Device {id} active profile changed to {}: {}",
|
||||
profile.index,
|
||||
profile.description
|
||||
);
|
||||
|
||||
let prev = self.active_profiles.insert(id, profile.clone());
|
||||
self.update_ui_profiles();
|
||||
if let Some(prev) = prev {
|
||||
if prev.index == profile.index {
|
||||
return;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
target: "sound",
|
||||
"Device {id} profile changed from {} to {}: {}",
|
||||
prev.index, profile.index, profile.description
|
||||
);
|
||||
} else {
|
||||
#[cfg(feature = "auto-profile-init")]
|
||||
if profile.index != 0 {
|
||||
// Use pw-cli to re-set the profile in case wireplumber has invalid state.
|
||||
// Profiles set by us do not need to use this. Only sets if profile is not `Off`.
|
||||
tracing::debug!(
|
||||
target: "sound",
|
||||
"Device {id} initialized with profile {}: {}", profile.index, profile.description
|
||||
);
|
||||
|
||||
self.set_profile(id, profile.index as u32, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::Event::ActiveRoute(id, _index, route) => {
|
||||
tracing::debug!(
|
||||
target: "sound",
|
||||
"Device {id} active route changed to {}: {}",
|
||||
route.index,
|
||||
route.description
|
||||
);
|
||||
|
||||
self.update_device_route_name(&route, id);
|
||||
|
||||
let (active_device, node_ids, set_default_node): (
|
||||
Option<DeviceId>,
|
||||
&[NodeId],
|
||||
fn(&mut Self, NodeId),
|
||||
) = match route.direction {
|
||||
pipewire::Direction::Output => (
|
||||
self.active_sink_device,
|
||||
&self.sink_node_ids,
|
||||
Self::set_default_sink_id,
|
||||
),
|
||||
pipewire::Direction::Input => (
|
||||
self.active_source_device,
|
||||
&self.source_node_ids,
|
||||
Self::set_default_source_id,
|
||||
),
|
||||
};
|
||||
|
||||
if active_device == Some(id) {
|
||||
for (node_id, &device) in &self.device_ids {
|
||||
if device == id && node_ids.contains(&node_id) {
|
||||
set_default_node(self, node_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::Event::AddProfile(id, index, profile) => {
|
||||
if let Some(p) = self.active_profiles.get_mut(id)
|
||||
&& p.index == profile.index
|
||||
{
|
||||
*p = profile.clone();
|
||||
}
|
||||
|
||||
let profiles = self.device_profiles.entry(id).or_default();
|
||||
if profiles.len() < index as usize + 1 {
|
||||
let additional = (index as usize + 1) - profiles.capacity();
|
||||
profiles.reserve_exact(additional);
|
||||
profiles.extend(std::iter::repeat_n(
|
||||
pipewire::Profile::default(),
|
||||
additional,
|
||||
));
|
||||
}
|
||||
|
||||
profiles[index as usize] = profile;
|
||||
|
||||
self.update_ui_profiles();
|
||||
}
|
||||
|
||||
pipewire::Event::AddRoute(id, index, route) => self.add_route(id, index, route),
|
||||
|
||||
pipewire::Event::AddDevice(device) => {
|
||||
tracing::debug!(target: "sound", "Device {} added: {}", device.id, device.name);
|
||||
self.device_names
|
||||
.insert(device.id, self.translate_device_name(&device.name));
|
||||
}
|
||||
|
||||
pipewire::Event::AddNode(node) => {
|
||||
tracing::debug!(target: "sound", "Node {} added: {}", node.object_id, node.node_name);
|
||||
// Device nodes will have device and card profile device IDs.
|
||||
// Virtual sinks/sources do not have these.
|
||||
if let Some(device_id) = node.device_id {
|
||||
self.device_ids.insert(node.object_id, device_id);
|
||||
|
||||
// This is the device number of the route. This is used with the
|
||||
// device ID to set properties for a route.
|
||||
if let Some(card_profile_device) = node.card_profile_device {
|
||||
self.card_profile_devices
|
||||
.insert(node.object_id, card_profile_device);
|
||||
}
|
||||
}
|
||||
|
||||
let description = self.translate_device_name(&node.description);
|
||||
|
||||
// The default sink/source is defined by a node's name. We use this when setting
|
||||
// virtual sink/source nodes with pactl; and when pipewire notifies us of a new
|
||||
// default sink/source.
|
||||
if self
|
||||
.node_names
|
||||
.insert(node.object_id, node.node_name.clone())
|
||||
.is_none()
|
||||
{
|
||||
// Use the device.profile.description as the route name by default for the UI.
|
||||
let name = if node.device_profile_description.is_empty() {
|
||||
description
|
||||
} else {
|
||||
[&node.device_profile_description, " - ", &description].concat()
|
||||
};
|
||||
|
||||
// Check if the node is a sink or a source, and append it to the relevant collections.
|
||||
match node.media_class {
|
||||
pipewire::MediaClass::Sink => {
|
||||
self.sinks.push(name);
|
||||
self.sink_node_ids.push(node.object_id);
|
||||
|
||||
// Set the sink as the default if it matches the server.
|
||||
if self.active_sink_node_name == node.node_name {
|
||||
tracing::debug!(
|
||||
target: "sound",
|
||||
"Node {} ({}) was the default sink",
|
||||
node.object_id,
|
||||
node.node_name
|
||||
);
|
||||
self.set_default_sink_node_id(node.object_id);
|
||||
} else if let Some(device_id) = self.changing_sink_device {
|
||||
for (node_id, &device) in &self.device_ids {
|
||||
if device == device_id && self.sink_node_ids.contains(&node_id)
|
||||
{
|
||||
self.changing_sink_device = None;
|
||||
self.set_default_sink_node_id(node_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::MediaClass::Source => {
|
||||
self.sources.push(name);
|
||||
self.source_node_ids.push(node.object_id);
|
||||
|
||||
// Set the source as the default if it matches the server.
|
||||
if self.active_source_node_name == node.node_name {
|
||||
tracing::debug!(
|
||||
target: "sound",
|
||||
"Node {} ({}) was the default source",
|
||||
node.object_id,
|
||||
node.node_name
|
||||
);
|
||||
self.set_default_source_node_id(node.object_id);
|
||||
} else if let Some(device_id) = self.changing_source_device {
|
||||
for (node_id, &device) in &self.device_ids {
|
||||
if device == device_id
|
||||
&& self.source_node_ids.contains(&node_id)
|
||||
{
|
||||
self.changing_source_device = None;
|
||||
self.set_default_source_node_id(node_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pipewire::Event::DefaultSink(node_name) => {
|
||||
tracing::debug!(target: "sound", "default sink node changed to {node_name}");
|
||||
if self.active_sink_node_name == node_name {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = self.node_id_from_name(&node_name) {
|
||||
self.set_default_sink_id(id);
|
||||
}
|
||||
|
||||
self.active_sink_node_name = node_name;
|
||||
}
|
||||
|
||||
pipewire::Event::DefaultSource(node_name) => {
|
||||
tracing::debug!(target: "sound", "default source node changed to {node_name}");
|
||||
if self.active_source_node_name == node_name {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(id) = self.node_id_from_name(&node_name) {
|
||||
self.set_default_source_id(id);
|
||||
}
|
||||
|
||||
self.active_source_node_name = node_name;
|
||||
}
|
||||
|
||||
pipewire::Event::RemoveDevice(id) => self.remove_device(id),
|
||||
pipewire::Event::RemoveNode(id) => self.remove_node(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_route(&mut self, id: DeviceId, index: u32, route: pipewire::Route) {
|
||||
self.update_device_route_name(&route, id);
|
||||
|
||||
tracing::debug!(target: "sound",
|
||||
"Device {} added route {} ({:?}); {:?}",
|
||||
id,
|
||||
route.name,
|
||||
route.direction,
|
||||
route.available
|
||||
);
|
||||
|
||||
let routes = self.device_routes.entry(id).or_default();
|
||||
if routes.len() < index as usize + 1 {
|
||||
let additional = (index as usize + 1) - routes.capacity();
|
||||
routes.reserve_exact(additional);
|
||||
routes.extend(std::iter::repeat_n(pipewire::Route::default(), additional));
|
||||
}
|
||||
|
||||
routes[index as usize] = route;
|
||||
}
|
||||
|
||||
fn node_id_from_name(&self, name: &str) -> Option<u32> {
|
||||
self.node_names
|
||||
.iter()
|
||||
.find(|&(_, n)| *n == name)
|
||||
.map(|(id, _)| id)
|
||||
}
|
||||
|
||||
fn remove_device(&mut self, id: DeviceId) {
|
||||
tracing::debug!(target: "sound", "Device {id} removed");
|
||||
_ = self.device_names.remove(id);
|
||||
_ = self.device_profiles.remove(id);
|
||||
_ = self.active_profiles.remove(id);
|
||||
_ = self.device_routes.remove(id);
|
||||
}
|
||||
|
||||
fn remove_node(&mut self, id: NodeId) {
|
||||
tracing::debug!(target: "sound", "Node {id} removed");
|
||||
if let Some(pos) = self.sink_node_ids.iter().position(|&node_id| node_id == id) {
|
||||
self.sink_node_ids.remove(pos);
|
||||
self.sinks.remove(pos);
|
||||
if let Some(node_id) = self.active_sink_node
|
||||
&& id == node_id
|
||||
{
|
||||
self.active_sink = None;
|
||||
self.active_sink_node = None;
|
||||
self.active_sink_node_name.clear();
|
||||
}
|
||||
} else if let Some(pos) = self
|
||||
.source_node_ids
|
||||
.iter()
|
||||
.position(|&node_id| node_id == id)
|
||||
{
|
||||
self.source_node_ids.remove(pos);
|
||||
self.sources.remove(pos);
|
||||
if let Some(node_id) = self.active_source_node
|
||||
&& id == node_id
|
||||
{
|
||||
self.active_source = None;
|
||||
self.active_source_node = None;
|
||||
self.active_source_node_name.clear();
|
||||
}
|
||||
}
|
||||
|
||||
_ = self.device_ids.remove(id);
|
||||
_ = self.node_names.remove(id);
|
||||
_ = self.card_profile_devices.remove(id);
|
||||
}
|
||||
|
||||
/// Set the default sink device by its the node ID.
|
||||
fn set_default_sink_id(&mut self, node_id: NodeId) {
|
||||
self.active_sink = self.sink_node_ids.iter().position(|&id| id == node_id);
|
||||
self.active_sink_node = Some(node_id);
|
||||
self.active_sink_node_name = self.node_names.get(node_id).cloned().unwrap_or_default();
|
||||
self.active_sink_device = self
|
||||
.device_ids
|
||||
.iter()
|
||||
.find_map(|(nid, did)| if nid == node_id { Some(*did) } else { None });
|
||||
}
|
||||
|
||||
/// Set the default source device by its the node ID.
|
||||
fn set_default_source_id(&mut self, node_id: NodeId) {
|
||||
self.active_source = self.source_node_ids.iter().position(|&id| id == node_id);
|
||||
self.active_source_node = Some(node_id);
|
||||
self.active_source_node_name = self.node_names.get(node_id).cloned().unwrap_or_default();
|
||||
self.active_source_device = self
|
||||
.device_ids
|
||||
.iter()
|
||||
.find_map(|(nid, did)| if nid == node_id { Some(*did) } else { None });
|
||||
}
|
||||
|
||||
fn update_device_route_name(&mut self, route: &pipewire::Route, id: DeviceId) {
|
||||
if matches!(route.available, Availability::No) {
|
||||
return;
|
||||
}
|
||||
|
||||
let (devices, node_ids) = match route.direction {
|
||||
pipewire::Direction::Output => (&mut self.sinks, &self.sink_node_ids),
|
||||
pipewire::Direction::Input => (&mut self.sources, &self.source_node_ids),
|
||||
};
|
||||
|
||||
for (pos, &node) in node_ids.iter().enumerate() {
|
||||
let Some(&device) = self.device_ids.get(node) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if device != id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(profile) = self.active_profiles.get(id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !profile.name.starts_with("pro-audio") {
|
||||
let Some(&card_profile_device) = self.card_profile_devices.get(node) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !route.devices.contains(&(card_profile_device as i32)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let Some(device_name) = self.device_names.get(id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
tracing::debug!(target: "sound", "matched route {} on {}: {}", route.index, id, route.description);
|
||||
devices[pos] = [&route.description, " - ", device_name].concat();
|
||||
self.node_route_indexes.insert(node, route.index);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the cached profiles for the UI.
|
||||
fn update_ui_profiles(&mut self) {
|
||||
self.device_profile_dropdowns = self
|
||||
.device_profiles
|
||||
.iter()
|
||||
.filter_map(|(device_id, profiles)| {
|
||||
let name = self.device_names.get(device_id)?.as_str();
|
||||
let (active_profile, indexes, descriptions) = self
|
||||
.active_profiles
|
||||
.get(device_id)
|
||||
.map(|profile| {
|
||||
let (indexes, descriptions): (Vec<_>, Vec<_>) = profiles
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.index == profile.index
|
||||
|| !matches!(p.available, pipewire::Availability::No)
|
||||
})
|
||||
.map(|p| (p.index as u32, p.description.clone()))
|
||||
.collect();
|
||||
|
||||
let pos = profiles
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.index == profile.index
|
||||
|| !matches!(p.available, pipewire::Availability::No)
|
||||
})
|
||||
.enumerate()
|
||||
.find(|(_, p)| p.index == profile.index)
|
||||
.map(|(pos, _)| pos);
|
||||
|
||||
(pos, indexes, descriptions)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let (indexes, descriptions): (Vec<_>, Vec<_>) = profiles
|
||||
.iter()
|
||||
.filter(|p| !matches!(p.available, pipewire::Availability::No))
|
||||
.map(|p| (p.index as u32, p.description.clone()))
|
||||
.collect();
|
||||
|
||||
(None, indexes, descriptions)
|
||||
});
|
||||
|
||||
Some((
|
||||
device_id,
|
||||
name.to_owned(),
|
||||
active_profile,
|
||||
indexes,
|
||||
descriptions,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.device_profile_dropdowns.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
}
|
||||
|
||||
fn translate_device_name(&self, input: &str) -> String {
|
||||
input
|
||||
.replacen(" Controller", "", 1)
|
||||
.replacen("High Definition Audio", &self.hd_audio_text, 1)
|
||||
.replacen("HD Audio", &self.hd_audio_text, 1)
|
||||
.replacen("USB Audio Device", &self.usb_audio_text, 1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
/// Handle messages from the sound server.
|
||||
Server(Arc<Vec<pipewire::Event>>),
|
||||
/// Change the output volume.
|
||||
SinkVolumeApply(NodeId),
|
||||
/// Change the input volume.
|
||||
SourceVolumeApply(NodeId),
|
||||
/// On init of the subscription, channels for closing background threads are given to the app.
|
||||
SubHandle(Arc<SubscriptionHandle>),
|
||||
}
|
||||
|
||||
pub struct SubscriptionHandle {
|
||||
cancel_tx: futures::channel::oneshot::Sender<()>,
|
||||
pipewire: pipewire::Sender,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SubscriptionHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("SubscriptionHandle")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use pipewire library
|
||||
pub async fn set_default(id: u32) {
|
||||
tracing::debug!(target: "sound", "setting default node {id}");
|
||||
let id = numtoa::BaseN::<10>::u32(id);
|
||||
_ = tokio::process::Command::new("wpctl")
|
||||
.args(["set-default", id.as_str()])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Use this to set a virtual sink as a default.
|
||||
/// TODO: We should be able to set this with pipewire-rs somehow.
|
||||
pub async fn pactl_set_default_sink(node_name: &str) {
|
||||
tracing::debug!(target: "sound", "setting default virtual node {node_name}");
|
||||
_ = tokio::process::Command::new("pactl")
|
||||
.args(["set-default-sink", node_name])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Use this to set a virtual sink as a default.
|
||||
/// TODO: We should be able to set this with pipewire-rs somehow.
|
||||
pub async fn pactl_set_default_source(node_name: &str) {
|
||||
_ = tokio::process::Command::new("pactl")
|
||||
.args(["set-default-source", node_name])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
|
||||
// TODO: Use pipewire library
|
||||
pub async fn set_profile(id: u32, index: u32, save: bool) {
|
||||
let id = numtoa::BaseN::<10>::u32(id);
|
||||
let index = numtoa::BaseN::<10>::u32(index);
|
||||
let value = [
|
||||
"{ index: ",
|
||||
index.as_str(),
|
||||
if save {
|
||||
", save: true }"
|
||||
} else {
|
||||
", save: false }"
|
||||
},
|
||||
]
|
||||
.concat();
|
||||
|
||||
_ = tokio::process::Command::new("pw-cli")
|
||||
.args(["s", id.as_str(), "Profile", &value])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.await;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue