Skip to content

Commit a76575b

Browse files
committed
Add PulseAudio support
This adds support for PulseAudio on hosts with a PA or PipeWire server (the latter via pipewire-pulse). Since the underlying client is async, some amount of bridging has to be done.
1 parent a5bd3c9 commit a76575b

File tree

8 files changed

+712
-81
lines changed

8 files changed

+712
-81
lines changed

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ rust-version = "1.70"
1212
[features]
1313
asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions.
1414
oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does.
15+
pulseaudio = ["dep:pulseaudio", "dep:futures"] # Only available on some Unix platforms.
1516

1617
[dependencies]
1718
dasp_sample = "0.11"
@@ -46,6 +47,8 @@ num-traits = { version = "0.2.6", optional = true }
4647
alsa = "0.9"
4748
libc = "0.2"
4849
jack = { version = "0.13.0", optional = true }
50+
pulseaudio = { git = "https://github.com/colinmarc/pulseaudio-rs", branch = "client", optional = true }
51+
futures = { version = "0.3", optional = true }
4952

5053
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
5154
core-foundation-sys = "0.8.2" # For linking to CoreFoundation.framework and handling device name `CFString`s.

examples/beep.rs

+38-39
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use clap::Parser;
22
use cpal::{
33
traits::{DeviceTrait, HostTrait, StreamTrait},
4-
FromSample, Sample, SizedSample, I24,
4+
FromSample, HostUnavailable, Sample, SizedSample, I24,
55
};
66

77
#[derive(Parser, Debug)]
@@ -11,58 +11,57 @@ struct Opt {
1111
#[arg(short, long, default_value_t = String::from("default"))]
1212
device: String,
1313

14-
/// Use the JACK host
15-
#[cfg(all(
16-
any(
17-
target_os = "linux",
18-
target_os = "dragonfly",
19-
target_os = "freebsd",
20-
target_os = "netbsd"
21-
),
22-
feature = "jack"
23-
))]
24-
#[arg(short, long)]
25-
#[allow(dead_code)]
14+
/// Use the JACK host. Requires `--features jack`.
15+
#[arg(long, default_value_t = false)]
2616
jack: bool,
17+
18+
/// Use the PulseAudio host. Requires `--features pulseaudio`.
19+
#[arg(long, default_value_t = false)]
20+
pulseaudio: bool,
2721
}
2822

2923
fn main() -> anyhow::Result<()> {
3024
let opt = Opt::parse();
3125

32-
// Conditionally compile with jack if the feature is specified.
33-
#[cfg(all(
34-
any(
35-
target_os = "linux",
36-
target_os = "dragonfly",
37-
target_os = "freebsd",
38-
target_os = "netbsd"
39-
),
40-
feature = "jack"
26+
// Jack/PulseAudio support must be enabled at compile time, and is
27+
// only available on some platforms.
28+
#[allow(unused_mut, unused_assignments)]
29+
let mut jack_host_id = Err(HostUnavailable);
30+
#[allow(unused_mut, unused_assignments)]
31+
let mut pulseaudio_host_id = Err(HostUnavailable);
32+
33+
#[cfg(any(
34+
target_os = "linux",
35+
target_os = "dragonfly",
36+
target_os = "freebsd",
37+
target_os = "netbsd"
4138
))]
39+
{
40+
#[cfg(feature = "jack")]
41+
{
42+
jack_host_id = Ok(cpal::HostId::Jack);
43+
}
44+
45+
#[cfg(feature = "pulseaudio")]
46+
{
47+
pulseaudio_host_id = Ok(cpal::HostId::PulseAudio);
48+
}
49+
}
50+
4251
// Manually check for flags. Can be passed through cargo with -- e.g.
4352
// cargo run --release --example beep --features jack -- --jack
4453
let host = if opt.jack {
45-
cpal::host_from_id(cpal::available_hosts()
46-
.into_iter()
47-
.find(|id| *id == cpal::HostId::Jack)
48-
.expect(
49-
"make sure --features jack is specified. only works on OSes where jack is available",
50-
)).expect("jack host unavailable")
54+
jack_host_id
55+
.and_then(cpal::host_from_id)
56+
.expect("make sure `--features jack` is specified, and the platform is supported")
57+
} else if opt.pulseaudio {
58+
pulseaudio_host_id
59+
.and_then(cpal::host_from_id)
60+
.expect("make sure `--features pulseaudio` is specified, and the platform is supported")
5161
} else {
5262
cpal::default_host()
5363
};
5464

55-
#[cfg(any(
56-
not(any(
57-
target_os = "linux",
58-
target_os = "dragonfly",
59-
target_os = "freebsd",
60-
target_os = "netbsd"
61-
)),
62-
not(feature = "jack")
63-
))]
64-
let host = cpal::default_host();
65-
6665
let device = if opt.device == "default" {
6766
host.default_output_device()
6867
} else {

examples/feedback.rs

+43-41
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
//! precisely synchronised.
88
99
use clap::Parser;
10-
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
10+
use cpal::{
11+
traits::{DeviceTrait, HostTrait, StreamTrait},
12+
HostUnavailable,
13+
};
1114
use ringbuf::{
1215
traits::{Consumer, Producer, Split},
1316
HeapRb,
@@ -28,58 +31,57 @@ struct Opt {
2831
#[arg(short, long, value_name = "DELAY_MS", default_value_t = 150.0)]
2932
latency: f32,
3033

31-
/// Use the JACK host
32-
#[cfg(all(
33-
any(
34-
target_os = "linux",
35-
target_os = "dragonfly",
36-
target_os = "freebsd",
37-
target_os = "netbsd"
38-
),
39-
feature = "jack"
40-
))]
41-
#[arg(short, long)]
42-
#[allow(dead_code)]
34+
/// Use the JACK host. Requires `--features jack`.
35+
#[arg(long, default_value_t = false)]
4336
jack: bool,
37+
38+
/// Use the PulseAudio host. Requires `--features pulseaudio`.
39+
#[arg(long, default_value_t = false)]
40+
pulseaudio: bool,
4441
}
4542

4643
fn main() -> anyhow::Result<()> {
4744
let opt = Opt::parse();
4845

49-
// Conditionally compile with jack if the feature is specified.
50-
#[cfg(all(
51-
any(
52-
target_os = "linux",
53-
target_os = "dragonfly",
54-
target_os = "freebsd",
55-
target_os = "netbsd"
56-
),
57-
feature = "jack"
46+
// Jack/PulseAudio support must be enabled at compile time, and is
47+
// only available on some platforms.
48+
#[allow(unused_mut, unused_assignments)]
49+
let mut jack_host_id = Err(HostUnavailable);
50+
#[allow(unused_mut, unused_assignments)]
51+
let mut pulseaudio_host_id = Err(HostUnavailable);
52+
53+
#[cfg(any(
54+
target_os = "linux",
55+
target_os = "dragonfly",
56+
target_os = "freebsd",
57+
target_os = "netbsd"
5858
))]
59+
{
60+
#[cfg(feature = "jack")]
61+
{
62+
jack_host_id = Ok(cpal::HostId::Jack);
63+
}
64+
65+
#[cfg(feature = "pulseaudio")]
66+
{
67+
pulseaudio_host_id = Ok(cpal::HostId::PulseAudio);
68+
}
69+
}
70+
5971
// Manually check for flags. Can be passed through cargo with -- e.g.
6072
// cargo run --release --example beep --features jack -- --jack
6173
let host = if opt.jack {
62-
cpal::host_from_id(cpal::available_hosts()
63-
.into_iter()
64-
.find(|id| *id == cpal::HostId::Jack)
65-
.expect(
66-
"make sure --features jack is specified. only works on OSes where jack is available",
67-
)).expect("jack host unavailable")
74+
jack_host_id
75+
.and_then(cpal::host_from_id)
76+
.expect("make sure `--features jack` is specified, and the platform is supported")
77+
} else if opt.pulseaudio {
78+
pulseaudio_host_id
79+
.and_then(cpal::host_from_id)
80+
.expect("make sure `--features pulseaudio` is specified, and the platform is supported")
6881
} else {
6982
cpal::default_host()
7083
};
7184

72-
#[cfg(any(
73-
not(any(
74-
target_os = "linux",
75-
target_os = "dragonfly",
76-
target_os = "freebsd",
77-
target_os = "netbsd"
78-
)),
79-
not(feature = "jack")
80-
))]
81-
let host = cpal::default_host();
82-
8385
// Find devices.
8486
let input_device = if opt.input_device == "default" {
8587
host.default_input_device()
@@ -164,8 +166,8 @@ fn main() -> anyhow::Result<()> {
164166
output_stream.play()?;
165167

166168
// Run for 3 seconds before closing.
167-
println!("Playing for 3 seconds... ");
168-
std::thread::sleep(std::time::Duration::from_secs(3));
169+
println!("Playing for 10 seconds... ");
170+
std::thread::sleep(std::time::Duration::from_secs(10));
169171
drop(input_stream);
170172
drop(output_stream);
171173
println!("Done!");

src/error.rs

+3
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,15 @@ impl From<BackendSpecificError> for DevicesError {
7070
pub enum DeviceNameError {
7171
/// See the [`BackendSpecificError`] docs for more information about this error variant.
7272
BackendSpecific { err: BackendSpecificError },
73+
/// The name is not valid UTF-8.
74+
InvalidUtf8,
7375
}
7476

7577
impl Display for DeviceNameError {
7678
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
7779
match self {
7880
Self::BackendSpecific { err } => err.fmt(f),
81+
Self::InvalidUtf8 => write!(f, "The name is not valid UTF-8"),
7982
}
8083
}
8184
}

src/host/mod.rs

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ pub(crate) mod jack;
2424
pub(crate) mod null;
2525
#[cfg(target_os = "android")]
2626
pub(crate) mod oboe;
27+
#[cfg(all(
28+
any(
29+
target_os = "linux",
30+
target_os = "dragonfly",
31+
target_os = "freebsd",
32+
target_os = "netbsd"
33+
),
34+
feature = "pulseaudio"
35+
))]
36+
pub(crate) mod pulseaudio;
2737
#[cfg(windows)]
2838
pub(crate) mod wasapi;
2939
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]

0 commit comments

Comments
 (0)