Skip to content

Support pipewire as host#1093

Open
Decodetalkers wants to merge 83 commits intoRustAudio:masterfrom
Decodetalkers:pipewire
Open

Support pipewire as host#1093
Decodetalkers wants to merge 83 commits intoRustAudio:masterfrom
Decodetalkers:pipewire

Conversation

@Decodetalkers
Copy link
Contributor

@Decodetalkers Decodetalkers commented Jan 6, 2026

Add support to pipewire

You can test it with pipewire feature open

Pipewire support use config to define rates. So the default config of cpal with pipewire can be changed through config like following.

cat ~/.config/pipewire/pipewire.conf.d/custom-rates.conf
context.properties = {
    default.clock.rate          = 48000
    default.clock.allowed-rates = [ 44100 48000 88200 96000 176400 192000 ]
}

Still problems left:

*once we do 'pipewire::init', we can only dequeue it after the whole thread. even put the function to another thread, the function still works.. But seems we can run init many times.. (Ok, seems it is not a problem, because in when we call init, the init action will only be called once. I think it will be ok)
*The crates by pipewire need edition 2024. I think that should be another pr.

@Decodetalkers Decodetalkers marked this pull request as draft January 6, 2026 12:20
@Decodetalkers Decodetalkers changed the title Pipewire support Support pipewire as host Jan 6, 2026
@Decodetalkers Decodetalkers marked this pull request as ready for review January 6, 2026 14:41
@Decodetalkers Decodetalkers force-pushed the pipewire branch 4 times, most recently from 9e2ef9f to fd59a29 Compare January 6, 2026 14:57
@Decodetalkers
Copy link
Contributor Author

Seems pipewire need edition 2024..

@Decodetalkers
Copy link
Contributor Author

@roderickvd can you help review this pr? Thanks, and when can this crate be upgraded to edition 2024? I would also want to help

@roderickvd
Copy link
Member

@roderickvd can you help review this pr?

Definitely will help you review it. Need a bit more time.

Thanks, and when can this crate be upgraded to edition 2024? I would also want to help

Actually cpal itself doesn't need to be upgraded to Rust 2024, it just needs a MSRV of Rust of 1.85 or higher to support dependencies that are Rust 2024 already.

When we can I'd like to stick cpal to Rust 2021 so we keep our MSRV down.

@SuperKenVery
Copy link
Contributor

Super cool man! Happy to see cpal having better linux support, opening the possibility of loopback recording on Linux!

@Decodetalkers Decodetalkers force-pushed the pipewire branch 3 times, most recently from 07bc999 to 6cea2ad Compare January 9, 2026 22:31
@Decodetalkers
Copy link
Contributor Author

ok, only one ci that I cannot fix

@Decodetalkers Decodetalkers force-pushed the pipewire branch 3 times, most recently from a81e104 to 5a3817c Compare January 10, 2026 16:19
@Decodetalkers
Copy link
Contributor Author

ok cross-rs based on ubuntu20.04, so

@Decodetalkers Decodetalkers force-pushed the pipewire branch 9 times, most recently from 721beb0 to 424d78f Compare January 11, 2026 09:53
@Be-ing
Copy link
Contributor

Be-ing commented Jan 15, 2026

How does this differ from #692?

Copy link
Member

@roderickvd roderickvd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really almost there 😆
Thanks for sticking through all this!

let current_channels = user_data.format.channels();
let current_rate = user_data.format.rate();
if current_channels != channels || rate != current_rate {
(user_data.error_callback)(StreamError::BackendSpecific {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to call stream.disconnect() or at least stream.set_active(false) on such an event. Because in the callback the n_channels is queried, and if we leave that running, then the data slice will be different from what the user expects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I set inactive when the values do not match.

mod stream;
mod utils;

// just init the pipewire the check if it is available
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment can be removed; function is self-explanatory.

channels: ChannelCount,
rate: SampleRate,
allow_rates: Vec<SampleRate>,
quantum: u32,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be of type FrameCount too.

&self.node_name
}

pub fn quantum(&self) -> FrameCount {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we should remove rate() and allow_rates() because they can be gotten from supported_*_configs already. quantum() is OK for now but I want to replace with the buffer size querying PR soon after.

//
// This ensures positive values that are compatible with our `StreamInstant` representation.
#[inline]
fn stream_timestamp_fallback(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's called "fallback" but do we ever use true PipeWire timestamps elsewhere with https://pipewire.pages.freedesktop.org/pipewire-rs/pipewire_sys/fn.pw_stream_get_time_n.html? We should: it's RT-safe and more accurate.

LLM-assisted sketch:

/// Hardware timestamp from a PipeWire graph cycle.
struct PwTime {
    /// CLOCK_MONOTONIC nanoseconds, stamped at the start of the graph cycle.
    now_ns: i64,
    /// Pipeline delay converted to nanoseconds.
    /// For output: how far ahead of the driver our next sample will be played.
    /// For input:  how long ago the data in the buffer was captured.
    delay_ns: i64,
}

/// Returns a hardware timestamp for the current graph cycle, or `None` if
/// the driver has not started yet or the rate is unavailable.
fn pw_stream_time(stream: &pw::stream::StreamRc) -> Option<(StreamInstant, i64)> {
    let mut t: pw_sys::pw_time = unsafe { mem::zeroed() };
    let rc = unsafe {
        pw_sys::pw_stream_get_time_n(
            stream.as_raw_ptr(),
            &mut t,
            mem::size_of::<pw_sys::pw_time>(),
        )
    };
    if rc != 0 || t.now == 0 || t.rate.denom == 0 {
        return None;
    }
    debug_assert_eq!(t.rate.num, 1, "unexpected pw_time rate.num");
    let delay_ns = t.delay * 1_000_000_000i64 / t.rate.denom as i64;
    let callback = crate::StreamInstant::from_nanos(t.now);
    Some((callback, delay_ns))
}

This may require features = ["v0_3_50"].

Then the StreamInstant docs should be updated to include PipeWire support.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@roderickvd
Copy link
Member

One more thing I just thought of: before passing the data buffer to the output callback, that buffer needs to be zeroed. More precisely: it needs to be set to Sample::EQUILIBRIUM (which is not "0" for unsigned formats). You could look at how ALSA does that with its silence_template.

@Decodetalkers
Copy link
Contributor Author

Decodetalkers commented Feb 28, 2026

One more thing I just thought of: before passing the data buffer to the output callback, that buffer needs to be zeroed. More precisely: it needs to be set to Sample::EQUILIBRIUM (which is not "0" for unsigned formats). You could look at how ALSA does that with its silence_template.

for example

let Some(samples) = buf_data.data() else {
       return;
};
let len = samples.len();
 let silence_template = vec![0_u8; len];
samples.copy_from_slice(&silence_template);

like this way? I do not know how to get the length of sample before the stream start..

@yara-blue
Copy link
Member

 let silence_template = vec![0_u8; len];

No that would allocate instead try calling https://doc.rust-lang.org/stable/std/primitive.slice.html#method.fill on the slice with Sample::EQUILIBRIUM as value.

@Decodetalkers
Copy link
Contributor Author

Decodetalkers commented Mar 1, 2026

 let silence_template = vec![0_u8; len];

No that would allocate instead try calling https://doc.rust-lang.org/stable/std/primitive.slice.html#method.fill on the slice with Sample::EQUILIBRIUM as value.

thank you very much, done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants