-
Notifications
You must be signed in to change notification settings - Fork 437
PulseAudio support #957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
PulseAudio support #957
Conversation
72c1c2f
to
a76575b
Compare
@colinmarc |
Pipewire and Pulseaudio are completely different protocols. Pulseaudio is the established linux audio server, and Pipewire is the new hotness. This PR implements the Pulseaudio protocol, while the other PR implements the Pipewire protocol. The other useful thing to know is that Pipewire (the server) supports the Pulse protocol as a first-class thing, and that this library has been tested with both audio servers. That means merging this would be enough to handle both cases. The PA server does not support the Pipewire protocol. Finally, this is just my opinion, but I think the Pipewire protocol is also significantly more complicated. |
I actually misread the issue and would have removed my comment if you weren't so quick to respond! 🤣
Right, since Pipewire could just interpret cpal through its PulseAudio interface. Thank you for the explanation :) |
@colinmarc new maintainer here and doing backlog grooming. So sorry this did not get picked up before, because it seems very worthwhile! Would you be so kind to resolve the conflicts so we can pick it up again? |
4aab015
to
cf32277
Compare
Great :) Just rebased and tests look good. Let me know if you want the first change as a separate PR (or feel free to just drop it). |
Wow, amazing turnaround time! 👍 Coming weeks I don't have access to a machine to test it myself, which, as much I believe you 😉 I would like to do. So for now I'm going to trigger an AI review - hope it's going to bring more value than hallucinations - and take some time for a code review over some days. This is a big contribution.
Yes, that'd be good if you could extract it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds PulseAudio support to CPAL as a new audio backend, addressing issue #259. The implementation introduces a new host type for PulseAudio/PipeWire compatibility and refactors the existing platform macro to improve type safety and maintainability.
- Adds comprehensive PulseAudio backend with input/output stream support and timing information
- Refactors the
impl_platform_host!
macro to use concrete types instead of module names for better type safety - Updates examples to support both JACK and PulseAudio host selection
Reviewed Changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
File | Description |
---|---|
src/platform/mod.rs | Refactors macro to use concrete types and adds PulseAudio host integration |
src/host/pulseaudio/mod.rs | Implements PulseAudio host, device enumeration, and stream configuration |
src/host/pulseaudio/stream.rs | Implements PulseAudio playback and record stream handling with timing |
src/host/null/mod.rs | Simplifies null host implementation using standard iterators |
src/host/mod.rs | Adds conditional compilation for PulseAudio module |
src/error.rs | Adds InvalidUtf8 variant to DeviceNameError |
examples/feedback.rs | Updates example to support PulseAudio host selection |
examples/beep.rs | Updates example to support PulseAudio host selection |
Cargo.toml | Adds pulseaudio and futures dependencies |
let bps = sample_spec.format.bytes_per_sample(); | ||
let n_samples = buf.len() / bps; | ||
let data = | ||
unsafe { Data::from_parts(buf.as_ptr() as *mut _, n_samples, sample_format) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Casting a const pointer to mutable is undefined behavior. The input buffer should remain const since it's read-only data. Consider using a different approach that doesn't violate pointer constness.
unsafe { Data::from_parts(buf.as_ptr() as *mut _, n_samples, sample_format) }; | |
Data::from_const_parts(buf.as_ptr(), n_samples, sample_format); |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done in many places in the codebase, and I don't have a better way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice if we could document how we ensure it's safety:
// SAFETY: We verify that:
// - buf.as_ptr() points to valid memory for at least n_samples * bytes_per_sample
// - n_samples is calculated from buf.len() / bytes_per_sample, ensuring validity
// - The buffer remains valid for the duration of the callback
// - sample_format matches the actual data layout in the buffer
|
||
// Spawn a thread to drive the stream future. | ||
let stream_clone = stream.clone(); | ||
let _worker_thread = std::thread::spawn(move || block_on(stream_clone.play_all())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The worker thread handle is dropped immediately, which means there's no way to properly join or manage the thread lifecycle. Consider storing the handle or using a different pattern for thread management.
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It exits when the stream finishes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to document that thread lifecycle.
Amazing pull request, but just one comment: in let client =
pulseaudio::Client::from_env(c"cpal-pulseaudio").map_err(|_| HostUnavailable)?; This means apps in the volume mixer will all show up as cpal-pulseaudio, when actually you'd want them to have their own name. It would be cool if I'm able to set this as well as other meta-data like the stream description. |
Thanks - where should I pull that from? I don't see a way to parameterize that on the generic host API. |
Hey @colinmarc, I didn't go through the code in any detail, but I gave your branch a quick test and it does work in my application. Pretty cool! |
This reduces the exported surface area of each host implementation, and makes the impl_platform_host macro more robust.
Cool, I rebased and added some fixes.
👉 #1004 👈 |
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.
My application now sometimes hangs when running it with the pulseaudio host.
Is as much useful information as I can share right now, I wasn't able to reproduce it outside of my application yet. I also don't know what causes it. |
Please run in debug and share the source, if possible. As it stands there's no way for me to know whether it's a bug in this PR, pulsaudio-rs, or your app. |
Thanks for the quick reply. I managed to get it to happen with a debug binary, I can't share the code of the application in which it happens but I'll try to reproduce it with standalone code or one of the examples but I can't get to it right now. Just wanted to let you know that there might be something.
rust log output
I can more or less reliably reproduce the issue in my application now by switching between devices. I tried adapting the record_wav sample to get the same result but failed so far. |
That was maddening to isolate but I can finally reproduce it outside of my application. The issue seems to happen when alsa and pulse audio devices exist at the same time, even if only one device has an active stream. Here is a crudely modified version of record_wav to reproduce the issue: I think arguably the main issue here is that the alsa device keeps an open handle around when it is not needed and not in the pulse integration. Even though it would be really nice if that failed with an error or timeout when the device is locked. Not keeping the devices around has it's issues too. As far as I know there is no reliable way to refer to a device in cpal other than keeping a reference to it. The only alternative seems to be to find the device by name, but I don't think there is any guarantee that the names are unique. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a couple of thoughts on the thread management. Thanks for you guys vetting this PR too. Let me know when you feel it's good to go.
let bps = sample_spec.format.bytes_per_sample(); | ||
let n_samples = buf.len() / bps; | ||
let data = | ||
unsafe { Data::from_parts(buf.as_ptr() as *mut _, n_samples, sample_format) }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice if we could document how we ensure it's safety:
// SAFETY: We verify that:
// - buf.as_ptr() points to valid memory for at least n_samples * bytes_per_sample
// - n_samples is calculated from buf.len() / bytes_per_sample, ensuring validity
// - The buffer remains valid for the duration of the callback
// - sample_format matches the actual data layout in the buffer
|
||
// Spawn a thread to drive the stream future. | ||
let stream_clone = stream.clone(); | ||
let _worker_thread = std::thread::spawn(move || block_on(stream_clone.play_all())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to document that thread lifecycle.
Fixes #259.
I finally got around to this after promising to PR it in #259 a year ago. 😅
This is based on colinmarc/pulseaudio-rs#2, which obviously needs to land before this. However, I developed both PRs in parallel. If you're feeling generous and would like to review that as well, I would welcome any feedback.
The first commit is unrelated and a bit opinionated, but it seemed nicer. Let me know if I should move that to a separate PR or just drop it.