Skip to content

feat(audio): native PipeWire capture backend on Linux - #1774

Open
olosegres wants to merge 1 commit into
cjpais:mainfrom
olosegres:pipewire-capture-backend
Open

feat(audio): native PipeWire capture backend on Linux#1774
olosegres wants to merge 1 commit into
cjpais:mainfrom
olosegres:pipewire-capture-backend

Conversation

@olosegres

@olosegres olosegres commented Jul 23, 2026

Copy link
Copy Markdown

Problem

On Linux, microphone capture is pinned to cpal's ALSA host (get_cpal_host()HostId::Alsa in audio_toolkit/audio/utils.rs). Handy's capture stream is therefore an ALSA "default" client, and PipeWire never sees it as a first-class node. On the (now near-universal) PipeWire desktop this means:

Change

Add a native PipeWire capture backend (pipewire-rs 0.10) that registers Handy's microphone as a real PipeWire node, so it shows up in wpctl status with its own settable volume and is routable per-app.

  • New pipewire_recorder.rs: runs the PipeWire main loop on a dedicated thread (all non-Send pw objects stay on it), negotiates F32, downmixes to mono in the RT process callback, and feeds the existing resampler → VAD → consumer pipeline unchanged via the shared Cmd/AudioChunk protocol. No DSP is reimplemented.
  • New recorder_backend.rs (Recorder): a thin seam that selects the backend. On Linux it prefers PipeWire and falls back to the cpal/ALSA path if PipeWire setup fails (e.g. no running session). Non-Linux targets compile cpal-only and are behaviourally unchanged; cpal stays fully compiled as the fallback everywhere.
  • pipewire is added only under [target.'cfg(target_os = "linux")'.dependencies] (feature v0_3_44), so Windows/macOS builds are untouched.

Testing

Built and run on Fedora (Asahi, aarch64) under GNOME/Wayland with PipeWire 1.6.8. Hotkey → record → transcribe works end-to-end through the native backend; the log shows Microphone capture using native PipeWire backend, and the capture node appears in wpctl status with a settable volume (wpctl set-volume <id> …) — which the old ALSA client did not support (Node … does not support volume).

Scope / follow-ups

This is an MVP that captures the default source (reproducing today's "default" behaviour). PipeWire-native device enumeration/selection is intentionally deferred — there's a TODO in Recorder::open, where a user-selected mic would map to a node.name passed through TARGET_OBJECT (already threaded through PipeWireRecorder::open). Happy to iterate on device selection and feature-gating in review.

@cjpais

cjpais commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Thank you I will take a look at this!

@olosegres

Copy link
Copy Markdown
Author

@cjpais any chance you've had a look at this? Been running it from my own fork build for about a month now — works great, zero complaints. Happy to rebase the conflicts if you're open to merging it 👍

On Linux, capture is pinned to cpal's ALSA host (`get_cpal_host` ->
`HostId::Alsa`), so Handy's microphone stream is an ALSA "default" client.
PipeWire cannot see it as a first-class node: there is no per-application
capture volume, no routing, and device enumeration only ever exposes
"default" instead of the graph's real sources.

Add a native PipeWire capture backend (pipewire-rs 0.10) that registers the
microphone stream as a real PipeWire node, so it appears in `wpctl status`
with its own settable volume and is routable per-app. It runs the PipeWire
main loop on a dedicated thread (all non-Send pw objects stay on it),
downmixes interleaved F32 to mono, and feeds the existing resampler -> VAD
-> consumer pipeline unchanged via the shared Cmd/AudioChunk protocol.

A small `Recorder` seam selects the backend: on Linux it prefers PipeWire
and falls back to the cpal/ALSA path if PipeWire setup fails; non-Linux
targets compile cpal-only and are unaffected. cpal stays fully compiled and
available as the fallback.

The MVP captures the default source (reproducing today's "default"
behaviour). PipeWire-native device enumeration/selection is left as a
follow-up (TODO in recorder_backend.rs, where a selected mic would map to a
node.name passed through TARGET_OBJECT).
@olosegres
olosegres force-pushed the pipewire-capture-backend branch from 8bec23c to 0a7bc41 Compare August 24, 2026 17:53
@cjpais

cjpais commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Okay, I'm gonna say I really want to merge this. I just need time to review. I'm gonna put this on my priority, so I appreciate you bumping this. This is definitely very important, and we need to merge this. I just need to check the implementation. I've probably been deferring it because it's a thousand lines of new code, but I think that this is very important. So let's definitely get this in ASAP.

Hopefully, we'll have a chance to at least do some minor review this morning and rebase it

@cjpais

cjpais commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Hey @olosegres I have some thoughts. I think it's probably worth doing device enumeration here. And another thing I'm wondering about is whether it makes sense on Linux to add an advanced setting for 'Audio Backend' or similar. With 3 options "Auto", "Pipewire", "ALSA". Part of the reason I'm thinking about this is that I want to make device enumeration clean, and I feel like the only way to do that is to basically have the app always know which backend is actually selected, and that auto would effectively do resolution and prefer pipewire. But if it doesn't exist, fall back. I'm curious what your thoughts are on this and if this is a set of changes that you think you're able to make. Also, I believe that we might need to add some dependencies in the CI to fix it as well if you're able to take a look at that. I do want to merge this. I'm just curious your thoughts and opinions since you're a primary Linux user and I'm not

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants