Skip to content

Commit 918a971

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 2b46b0b commit 918a971

File tree

8 files changed

+712
-83
lines changed

8 files changed

+712
-83
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
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

Lines changed: 38 additions & 40 deletions
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,56 @@ 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"
41-
))]
26+
// Jack/PulseAudio support must be enabled at compile time, and is
27+
// only available on some platforms.
28+
#[allow(unused_mut)]
29+
let mut jack_host_id = Err(HostUnavailable);
30+
#[allow(unused_mut)]
31+
let mut pulseaudio_host_id = Err(HostUnavailable);
32+
33+
if cfg!(any(
34+
target_os = "linux",
35+
target_os = "dragonfly",
36+
target_os = "freebsd",
37+
target_os = "netbsd"
38+
)) {
39+
#[cfg(feature = "jack")]
40+
{
41+
jack_host_id = Ok(cpal::HostId::Jack);
42+
}
43+
44+
#[cfg(feature = "pulseaudio")]
45+
{
46+
pulseaudio_host_id = Ok(cpal::HostId::PulseAudio);
47+
}
48+
}
49+
4250
// Manually check for flags. Can be passed through cargo with -- e.g.
4351
// cargo run --release --example beep --features jack -- --jack
4452
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")
53+
jack_host_id
54+
.and_then(cpal::host_from_id)
55+
.expect("make sure `--features jack` is specified, and the platform is supported")
56+
} else if opt.pulseaudio {
57+
pulseaudio_host_id
58+
.and_then(cpal::host_from_id)
59+
.expect("make sure `--features pulseaudio` is specified, and the platform is supported")
5160
} else {
5261
cpal::default_host()
5362
};
5463

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-
6664
let device = if opt.device == "default" {
6765
host.default_output_device()
6866
} else {

examples/feedback.rs

Lines changed: 43 additions & 42 deletions
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,56 @@ 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"
58-
))]
46+
// Jack/PulseAudio support must be enabled at compile time, and is
47+
// only available on some platforms.
48+
#[allow(unused_mut)]
49+
let mut jack_host_id = Err(HostUnavailable);
50+
#[allow(unused_mut)]
51+
let mut pulseaudio_host_id = Err(HostUnavailable);
52+
53+
if cfg!(any(
54+
target_os = "linux",
55+
target_os = "dragonfly",
56+
target_os = "freebsd",
57+
target_os = "netbsd"
58+
)) {
59+
#[cfg(feature = "jack")]
60+
{
61+
jack_host_id = Ok(cpal::HostId::Jack);
62+
}
63+
64+
#[cfg(feature = "pulseaudio")]
65+
{
66+
pulseaudio_host_id = Ok(cpal::HostId::PulseAudio);
67+
}
68+
}
69+
5970
// Manually check for flags. Can be passed through cargo with -- e.g.
6071
// cargo run --release --example beep --features jack -- --jack
6172
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")
73+
jack_host_id
74+
.and_then(cpal::host_from_id)
75+
.expect("make sure `--features jack` is specified, and the platform is supported")
76+
} else if opt.pulseaudio {
77+
pulseaudio_host_id
78+
.and_then(cpal::host_from_id)
79+
.expect("make sure `--features pulseaudio` is specified, and the platform is supported")
6880
} else {
6981
cpal::default_host()
7082
};
7183

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-
8384
// Find devices.
8485
let input_device = if opt.input_device == "default" {
8586
host.default_input_device()
@@ -164,8 +165,8 @@ fn main() -> anyhow::Result<()> {
164165
output_stream.play()?;
165166

166167
// Run for 3 seconds before closing.
167-
println!("Playing for 3 seconds... ");
168-
std::thread::sleep(std::time::Duration::from_secs(3));
168+
println!("Playing for 10 seconds... ");
169+
std::thread::sleep(std::time::Duration::from_secs(10));
169170
drop(input_stream);
170171
drop(output_stream);
171172
println!("Done!");

src/error.rs

Lines changed: 3 additions & 0 deletions
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

Lines changed: 10 additions & 0 deletions
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)