Skip to content

Commit ac9a8b3

Browse files
authored
Merge pull request #2166 from bim9262/pipewire_sound
Add pipewire sound driver
2 parents 59eefca + 303c5ea commit ac9a8b3

File tree

8 files changed

+1260
-286
lines changed

8 files changed

+1260
-286
lines changed

Cargo.lock

Lines changed: 60 additions & 102 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 & 1 deletion
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.9"
3536
bytes = "1.8"
3637
calibright = { version = "0.1.13", features = ["watch"] }
3738
chrono = { version = "0.4", default-features = false, features = ["clock", "unstable-locales"] }
@@ -60,7 +61,7 @@ nom = "7.1.2"
6061
notmuch = { version = "0.8", optional = true }
6162
oauth2 = { version = "5.0.0", default-features = false, features = ["reqwest"] }
6263
num-traits = "0.2"
63-
pipewire = { version = "0.8", default-features = false, optional = true }
64+
pipewire = { version = "0.9", default-features = false, optional = true }
6465
quick-xml = { version = "0.37", features = ["serialize"] }
6566
regex = "1.5"
6667
reqwest = { version = "0.12", features = ["json"] }

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
@@ -188,8 +190,8 @@ words:
188190
- xclip
189191
- xcolors
190192
- xesam
191-
- xkbswitch
192193
- xkbevent
194+
- xkbswitch
193195
- XKCD
194196
- xrandr
195197
- xresources

src/blocks/privacy/pipewire.rs

Lines changed: 19 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +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) = global_props
52-
.get(&keys::LINK_OUTPUT_NODE)
53-
.and_then(|s| s.parse().ok())
54-
&& let Some(link_input_node) = global_props
55-
.get(&keys::LINK_INPUT_NODE)
56-
.and_then(|s| s.parse().ok())
57-
{
58-
Some(Self {
59-
link_output_node,
60-
link_input_node,
61-
})
62-
} else {
63-
None
64-
}
65-
}
66-
}
67-
68-
#[derive(Default)]
69-
struct Data {
70-
nodes: HashMap<u32, Node>,
71-
links: HashMap<u32, Link>,
72-
}
73-
74-
#[derive(Default)]
75-
struct Client {
76-
event_listeners: Mutex<Vec<Weak<Notify>>>,
77-
data: Mutex<Data>,
78-
}
79-
80-
impl Client {
81-
fn new() -> Result<Client> {
82-
thread::Builder::new()
83-
.name("privacy_pipewire".to_string())
84-
.spawn(Client::main_loop_thread)
85-
.error("failed to spawn a thread")?;
86-
87-
Ok(Client::default())
88-
}
89-
90-
fn main_loop_thread() {
91-
let client = CLIENT.as_ref().error("Could not get client").unwrap();
92-
93-
let proplist = properties! {*keys::APP_NAME => env!("CARGO_PKG_NAME")};
94-
95-
let main_loop = MainLoop::new(None).expect("Failed to create main loop");
96-
97-
let context =
98-
Context::with_properties(&main_loop, proplist).expect("Failed to create context");
99-
let core = context.connect(None).expect("Failed to connect");
100-
let registry = core.get_registry().expect("Failed to get registry");
101-
102-
let updated = Rc::new(Cell::new(false));
103-
let updated_copy = updated.clone();
104-
let updated_copy2 = updated.clone();
105-
106-
// Register a callback to the `global` event on the registry, which notifies of any new global objects
107-
// appearing on the remote.
108-
// The callback will only get called as long as we keep the returned listener alive.
109-
let _registry_listener = registry
110-
.add_listener_local()
111-
.global(move |global| {
112-
let Some(global_props) = global.props else {
113-
return;
114-
};
115-
match &global.type_ {
116-
ObjectType::Node => {
117-
client
118-
.data
119-
.lock()
120-
.unwrap()
121-
.nodes
122-
.insert(global.id, Node::new(global.id, global_props));
123-
updated_copy.set(true);
124-
}
125-
ObjectType::Link => {
126-
let Some(link) = Link::new(global_props) else {
127-
return;
128-
};
129-
client.data.lock().unwrap().links.insert(global.id, link);
130-
updated_copy.set(true);
131-
}
132-
_ => (),
133-
}
134-
})
135-
.global_remove(move |uid| {
136-
let mut data = client.data.lock().unwrap();
137-
if data.nodes.remove(&uid).is_some() || data.links.remove(&uid).is_some() {
138-
updated_copy2.set(true);
139-
}
140-
})
141-
.register();
142-
143-
loop {
144-
main_loop.loop_().iterate(Duration::from_secs(60 * 60 * 24));
145-
if updated.get() {
146-
updated.set(false);
147-
client
148-
.event_listeners
149-
.lock()
150-
.unwrap()
151-
.retain(|notify| notify.upgrade().inspect(|x| x.notify_one()).is_some());
152-
}
153-
}
154-
}
155-
156-
fn add_event_listener(&self, notify: &Arc<Notify>) {
157-
self.event_listeners
158-
.lock()
159-
.unwrap()
160-
.push(Arc::downgrade(notify));
161-
}
162-
}
5+
use crate::pipewire::{CLIENT, EventKind, Link, Node};
1636

1647
#[derive(Deserialize, Debug, SmartDefault)]
1658
#[serde(rename_all = "lowercase", deny_unknown_fields, default)]
@@ -190,15 +33,18 @@ impl NodeDisplay {
19033

19134
pub(super) struct Monitor<'a> {
19235
config: &'a Config,
193-
notify: Arc<Notify>,
36+
updates: UnboundedReceiver<EventKind>,
19437
}
19538

19639
impl<'a> Monitor<'a> {
19740
pub(super) async fn new(config: &'a Config) -> Result<Self> {
19841
let client = CLIENT.as_ref().error("Could not get client")?;
199-
let notify = Arc::new(Notify::new());
200-
client.add_event_listener(&notify);
201-
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+
})
20248
}
20349
}
20450

@@ -255,7 +101,16 @@ impl PrivacyMonitor for Monitor<'_> {
255101
}
256102

257103
async fn wait_for_change(&mut self) -> Result<()> {
258-
self.notify.notified().await;
104+
while let Some(event) = self.updates.recv().await {
105+
if event.intersects(
106+
EventKind::NODE_ADDED
107+
| EventKind::NODE_REMOVED
108+
| EventKind::LINK_ADDED
109+
| EventKind::LINK_REMOVED,
110+
) {
111+
break;
112+
}
113+
}
259114
Ok(())
260115
}
261116
}

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 {
@@ -329,6 +334,8 @@ pub enum SoundDriver {
329334
#[default]
330335
Auto,
331336
Alsa,
337+
#[cfg(feature = "pipewire")]
338+
Pipewire,
332339
#[cfg(feature = "pulseaudio")]
333340
PulseAudio,
334341
}

0 commit comments

Comments
 (0)