Skip to content

Commit fee6ef2

Browse files
committed
Add pipewire sound driver
Fixies #2164
1 parent 50c611e commit fee6ef2

File tree

8 files changed

+988
-184
lines changed

8 files changed

+988
-184
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ rustdoc-args = ["--cfg", "docsrs"]
3232
async-trait = "0.1"
3333
backon = { version = "1.2", default-features = false, features = ["tokio-sleep"] }
3434
base64 = { version = "0.22.1" }
35+
bitflags = "2.6"
3536
calibright = { version = "0.1.9", features = ["watch"] }
3637
chrono = { version = "0.4", default-features = false, features = ["clock", "unstable-locales"] }
3738
chrono-tz = { version = "0.10", features = ["serde"] }
@@ -51,6 +52,7 @@ inotify = "0.11"
5152
itertools = "0.13"
5253
libc = "0.2"
5354
libpulse-binding = { version = "2.0", default-features = false, optional = true }
55+
libspa = "0.8.0"
5456
log = "0.4"
5557
maildir = { version = "0.6", optional = true }
5658
neli = { version = "0.6", features = ["async"] }

cspell.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ words:
3131
- bugz
3232
- busctl
3333
- caldav
34+
- cbrt
3435
- ccache
3536
- chrono
3637
- clippy
@@ -86,6 +87,7 @@ words:
8687
- kibi
8788
- kmon
8889
- libc
90+
- libdbus
8991
- liquidctl
9092
- locid
9193
- macchiato
@@ -187,8 +189,8 @@ words:
187189
- xclip
188190
- xcolors
189191
- xesam
190-
- xkbswitch
191192
- xkbevent
193+
- xkbswitch
192194
- XKCD
193195
- xrandr
194196
- xresources

src/blocks/privacy/pipewire.rs

Lines changed: 19 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,8 @@
1-
use std::cell::Cell;
2-
use std::collections::HashMap;
3-
use std::rc::Rc;
4-
use std::sync::{Arc, Mutex, Weak};
5-
use std::thread;
6-
7-
use ::pipewire::{
8-
context::Context, keys, main_loop::MainLoop, properties::properties, spa::utils::dict::DictRef,
9-
types::ObjectType,
10-
};
111
use itertools::Itertools as _;
12-
use tokio::sync::Notify;
2+
use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
133

144
use super::*;
15-
16-
static CLIENT: LazyLock<Result<Client>> = LazyLock::new(Client::new);
17-
18-
#[derive(Debug)]
19-
struct Node {
20-
name: String,
21-
nick: Option<String>,
22-
media_class: Option<String>,
23-
media_role: Option<String>,
24-
description: Option<String>,
25-
}
26-
27-
impl Node {
28-
fn new(global_id: u32, global_props: &DictRef) -> Self {
29-
Self {
30-
name: global_props
31-
.get(&keys::NODE_NAME)
32-
.map_or_else(|| format!("node_{}", global_id), |s| s.to_string()),
33-
nick: global_props.get(&keys::NODE_NICK).map(|s| s.to_string()),
34-
media_class: global_props.get(&keys::MEDIA_CLASS).map(|s| s.to_string()),
35-
media_role: global_props.get(&keys::MEDIA_ROLE).map(|s| s.to_string()),
36-
description: global_props
37-
.get(&keys::NODE_DESCRIPTION)
38-
.map(|s| s.to_string()),
39-
}
40-
}
41-
}
42-
43-
#[derive(Debug, PartialEq, PartialOrd, Eq, Ord)]
44-
struct Link {
45-
link_output_node: u32,
46-
link_input_node: u32,
47-
}
48-
49-
impl Link {
50-
fn new(global_props: &DictRef) -> Option<Self> {
51-
if let (Some(link_output_node), Some(link_input_node)) = (
52-
global_props
53-
.get(&keys::LINK_OUTPUT_NODE)
54-
.and_then(|s| s.parse().ok()),
55-
global_props
56-
.get(&keys::LINK_INPUT_NODE)
57-
.and_then(|s| s.parse().ok()),
58-
) {
59-
Some(Self {
60-
link_output_node,
61-
link_input_node,
62-
})
63-
} else {
64-
None
65-
}
66-
}
67-
}
68-
69-
#[derive(Default)]
70-
struct Data {
71-
nodes: HashMap<u32, Node>,
72-
links: HashMap<u32, Link>,
73-
}
74-
75-
#[derive(Default)]
76-
struct Client {
77-
event_listeners: Mutex<Vec<Weak<Notify>>>,
78-
data: Mutex<Data>,
79-
}
80-
81-
impl Client {
82-
fn new() -> Result<Client> {
83-
thread::Builder::new()
84-
.name("privacy_pipewire".to_string())
85-
.spawn(Client::main_loop_thread)
86-
.error("failed to spawn a thread")?;
87-
88-
Ok(Client::default())
89-
}
90-
91-
fn main_loop_thread() {
92-
let client = CLIENT.as_ref().error("Could not get client").unwrap();
93-
94-
let proplist = properties! {*keys::APP_NAME => env!("CARGO_PKG_NAME")};
95-
96-
let main_loop = MainLoop::new(None).expect("Failed to create main loop");
97-
98-
let context =
99-
Context::with_properties(&main_loop, proplist).expect("Failed to create context");
100-
let core = context.connect(None).expect("Failed to connect");
101-
let registry = core.get_registry().expect("Failed to get registry");
102-
103-
let updated = Rc::new(Cell::new(false));
104-
let updated_copy = updated.clone();
105-
let updated_copy2 = updated.clone();
106-
107-
// Register a callback to the `global` event on the registry, which notifies of any new global objects
108-
// appearing on the remote.
109-
// The callback will only get called as long as we keep the returned listener alive.
110-
let _registry_listener = registry
111-
.add_listener_local()
112-
.global(move |global| {
113-
let Some(global_props) = global.props else {
114-
return;
115-
};
116-
match &global.type_ {
117-
ObjectType::Node => {
118-
client
119-
.data
120-
.lock()
121-
.unwrap()
122-
.nodes
123-
.insert(global.id, Node::new(global.id, global_props));
124-
updated_copy.set(true);
125-
}
126-
ObjectType::Link => {
127-
let Some(link) = Link::new(global_props) else {
128-
return;
129-
};
130-
client.data.lock().unwrap().links.insert(global.id, link);
131-
updated_copy.set(true);
132-
}
133-
_ => (),
134-
}
135-
})
136-
.global_remove(move |uid| {
137-
let mut data = client.data.lock().unwrap();
138-
if data.nodes.remove(&uid).is_some() || data.links.remove(&uid).is_some() {
139-
updated_copy2.set(true);
140-
}
141-
})
142-
.register();
143-
144-
loop {
145-
main_loop.loop_().iterate(Duration::from_secs(60 * 60 * 24));
146-
if updated.get() {
147-
updated.set(false);
148-
client
149-
.event_listeners
150-
.lock()
151-
.unwrap()
152-
.retain(|notify| notify.upgrade().inspect(|x| x.notify_one()).is_some());
153-
}
154-
}
155-
}
156-
157-
fn add_event_listener(&self, notify: &Arc<Notify>) {
158-
self.event_listeners
159-
.lock()
160-
.unwrap()
161-
.push(Arc::downgrade(notify));
162-
}
163-
}
5+
use crate::pipewire::{CLIENT, EventKind, Link, Node};
1646

1657
#[derive(Deserialize, Debug, SmartDefault)]
1668
#[serde(rename_all = "lowercase", deny_unknown_fields, default)]
@@ -191,15 +33,18 @@ impl NodeDisplay {
19133

19234
pub(super) struct Monitor<'a> {
19335
config: &'a Config,
194-
notify: Arc<Notify>,
36+
updates: UnboundedReceiver<EventKind>,
19537
}
19638

19739
impl<'a> Monitor<'a> {
19840
pub(super) async fn new(config: &'a Config) -> Result<Self> {
19941
let client = CLIENT.as_ref().error("Could not get client")?;
200-
let notify = Arc::new(Notify::new());
201-
client.add_event_listener(&notify);
202-
Ok(Self { config, notify })
42+
let (tx, rx) = unbounded_channel();
43+
client.add_event_listener(tx);
44+
Ok(Self {
45+
config,
46+
updates: rx,
47+
})
20348
}
20449
}
20550

@@ -260,7 +105,16 @@ impl PrivacyMonitor for Monitor<'_> {
260105
}
261106

262107
async fn wait_for_change(&mut self) -> Result<()> {
263-
self.notify.notified().await;
108+
while let Some(event) = self.updates.recv().await {
109+
if event.intersects(
110+
EventKind::NODE_ADDED
111+
| EventKind::NODE_REMOVED
112+
| EventKind::LINK_ADDED
113+
| EventKind::LINK_REMOVED,
114+
) {
115+
break;
116+
}
117+
}
264118
Ok(())
265119
}
266120
}

src/blocks/sound.rs

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//!
1111
//! Key | Values | Default
1212
//! ----|--------|--------
13-
//! `driver` | `"auto"`, `"pulseaudio"`, `"alsa"`. | `"auto"` (Pulseaudio with ALSA fallback)
13+
//! `driver` | `"auto"`, `pipewire`, `"pulseaudio"`, `"alsa"`. | `"auto"` (Pipewire with Pulseaudio fallback with ALSA fallback)
1414
//! `format` | A string to customise the output of this block. See below for available placeholders. | <code>\" $icon {$volume.eng(w:2) \|}\"</code>
1515
//! `format_alt` | If set, block will switch between `format` and `format_alt` on every click. | `None`
1616
//! `name` | PulseAudio device name, or the ALSA control name as found in the output of `amixer -D yourdevice scontrols`. | PulseAudio: `@DEFAULT_SINK@` / ALSA: `Master`
@@ -92,7 +92,11 @@
9292
//! - `volume` (as a progression)
9393
//! - `headphones`
9494
95+
make_log_macro!(debug, "sound");
96+
9597
mod alsa;
98+
#[cfg(feature = "pipewire")]
99+
pub mod pipewire;
96100
#[cfg(feature = "pulseaudio")]
97101
mod pulseaudio;
98102

@@ -101,8 +105,6 @@ use crate::wrappers::SerdeRegex;
101105
use indexmap::IndexMap;
102106
use regex::Regex;
103107

104-
make_log_macro!(debug, "sound");
105-
106108
#[derive(Deserialize, Debug, SmartDefault)]
107109
#[serde(deny_unknown_fields, default)]
108110
pub struct Config {
@@ -188,29 +190,32 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
188190
config.device.clone().unwrap_or_else(|| "default".into()),
189191
config.natural_mapping,
190192
)?),
193+
#[cfg(feature = "pipewire")]
194+
SoundDriver::Pipewire => {
195+
Box::new(pipewire::Device::new(config.device_kind, config.name.clone()).await?)
196+
}
191197
#[cfg(feature = "pulseaudio")]
192198
SoundDriver::PulseAudio => Box::new(pulseaudio::Device::new(
193199
config.device_kind,
194200
config.name.clone(),
195201
)?),
196-
#[cfg(feature = "pulseaudio")]
197-
SoundDriver::Auto => {
202+
SoundDriver::Auto => 'blk: {
203+
#[cfg(feature = "pipewire")]
204+
if let Ok(pipewire) =
205+
pipewire::Device::new(config.device_kind, config.name.clone()).await
206+
{
207+
break 'blk Box::new(pipewire);
208+
}
209+
#[cfg(feature = "pulseaudio")]
198210
if let Ok(pulse) = pulseaudio::Device::new(config.device_kind, config.name.clone()) {
199-
Box::new(pulse)
200-
} else {
201-
Box::new(alsa::Device::new(
202-
config.name.clone().unwrap_or_else(|| "Master".into()),
203-
config.device.clone().unwrap_or_else(|| "default".into()),
204-
config.natural_mapping,
205-
)?)
211+
break 'blk Box::new(pulse);
206212
}
213+
Box::new(alsa::Device::new(
214+
config.name.clone().unwrap_or_else(|| "Master".into()),
215+
config.device.clone().unwrap_or_else(|| "default".into()),
216+
config.natural_mapping,
217+
)?)
207218
}
208-
#[cfg(not(feature = "pulseaudio"))]
209-
SoundDriver::Auto => Box::new(alsa::Device::new(
210-
config.name.clone().unwrap_or_else(|| "Master".into()),
211-
config.device.clone().unwrap_or_else(|| "default".into()),
212-
config.natural_mapping,
213-
)?),
214219
};
215220

216221
let mappings = match &config.mappings {
@@ -330,6 +335,8 @@ pub enum SoundDriver {
330335
#[default]
331336
Auto,
332337
Alsa,
338+
#[cfg(feature = "pipewire")]
339+
Pipewire,
333340
#[cfg(feature = "pulseaudio")]
334341
PulseAudio,
335342
}

0 commit comments

Comments
 (0)