Skip to content

[Linux/rust] Implemented high-res microphone support#655

Open
LuanAdemi wants to merge 14 commits into
librepods-org:linux/rustfrom
LuanAdemi:linux/rust
Open

[Linux/rust] Implemented high-res microphone support#655
LuanAdemi wants to merge 14 commits into
librepods-org:linux/rustfrom
LuanAdemi:linux/rust

Conversation

@LuanAdemi

@LuanAdemi LuanAdemi commented Jun 26, 2026

Copy link
Copy Markdown

Added support for Apple’s proprietary high-resolution microphone stream

image

A huge thank you to everyone in the reverse-engineering Discord channel who helped make this possible. Special thanks to @kavishdevar for the initial discovery, Bluetooth packet captures, and for figuring out the correct AACP command sequence.

How it works

AirPods are capable of transmitting a high-resolution mono microphone stream using Apple’s AACP protocol, while simultaneously maintaining high-quality audio playback over A2DP.

This pull request adds support for:

  • Required AACP control commands
  • Audio packet framing for the uplink stream
  • AAC-ELD decoding via FFmpeg’s libavcodec (LGPL-licensed)
  • PipeWire integration using a virtual input device

TODO

  • Check license compatibility (especially if we need a patent license)

AI usage

The code in this pull request was refactored using Claude Code since I have little experience with rust and my rust code sucks. I carefully reviewed and tested every ai generated code snipped.

Adds AAC-ELD decoding using ffmpegs libavcodec and pipes the decoded
0x58 AACP stream into a virtual PipeWire input.
@LuanAdemi LuanAdemi mentioned this pull request Jun 26, 2026
26 tasks
The capture stream and decoder thread are now started or stopped
automatically based on whether an application is actively recording
from the persistent virtual input device.
Introduce a centralized AppSettings struct to handle loading and
saving of application configuration.
The main change is a watchdog pattern that handles missing SDUs by
restarting the capture after N SDUs got lost.
corrupt connection.

Exiting using the tray did not teardown the mic stream, leaving the
connection in a corrupted state.
@kavishdevar

kavishdevar commented Jul 1, 2026

Copy link
Copy Markdown
Member

Two things:

  • Conversational awareness should be disabled when mic is active. not just the callbacks, we should be sending the ctrl cmd
  • The a2dp transport reset causes a switch to Headset profile, and a2dp profiles aren't available anymore. instead of suspending the sink, we can set the card profile to off and then back to a2dp (without sleep) and this fixes the audio playing from only one of the airpods

LuanAdemi added 3 commits July 2, 2026 14:09
Add a setting to automatically disable conversation detection
when the Hi-Res microphone is capturing, restoring the previous
state after capture ends.
capture.

Disabling the high-res mic during capture removed the input device,
making pipewire switch to the HFP mic.
@LuanAdemi

LuanAdemi commented Jul 2, 2026

Copy link
Copy Markdown
Author

Two things:

* Conversational awareness should be disabled when mic is active. not just the callbacks, we should be sending the ctrl cmd

* The a2dp transport reset causes a switch to Headset profile, and a2dp profiles aren't available anymore. instead of suspending the sink, we can set the card profile to off and then back to a2dp (without sleep) and this fixes the audio playing from only one of the airpods

Done. Your changes went missing for some reason. I used your card profile code snippet in the last commit.

@randshell

Copy link
Copy Markdown

I’m not sure what happened, but I don’t hear any audio anymore on the hires output when the hires microphone is active. The hires microphone seems to be working though.

I’ve tried various builds / commits to no avail. The issue started around the time when I put the AirPods back in their case without disconnecting them first and while hires audio was on. Rebooting and switching on and off hires mic didn’t help either.

@LuanAdemi

LuanAdemi commented Jul 9, 2026

Copy link
Copy Markdown
Author

I’m not sure what happened, but I don’t hear any audio anymore on the hires output when the hires microphone is active. The hires microphone seems to be working though.

I’ve tried various builds / commits to no avail. The issue started around the time when I put the AirPods back in their case without disconnecting them first and while hires audio was on. Rebooting and switching on and off hires mic didn’t help either.

Do understand correctly: you can't hear any audio (e.g. playing a YouTube video), but the mic works correctly? Can you provide some more information about your environment?

@randshell

Copy link
Copy Markdown

@LuanAdemi I'm on the latest Fedora 44 Kinoite.

When I'm on AirPods_HiRes_Sink, I can't hear anything and I've noticed it's a mono sink. However the AirPods_HiRes_Mic records in high-quality, which I can only check when disabling the Hi-Res feature and going back to the normal sink playback.

Another thing I've noticed is that when I switch the playback from AirPods_HiRes_Sink to the normal one while the Hi-Res is on, I can hear the sounds but the microphone stream gets added too, so I hear both the playback and the real-time Hi-Res microphone. I'm not sure if this is a technical limitation, but maybe it helps with debugging the issue.

Below some extra info. Let me know what else you'd need.

`$ pw-cli ls`
[...]
        id 82, type PipeWire:Interface:Node/3
                object.serial = "3763"
                factory.id = "12"
                client.id = "218"
                node.description = "Bluetooth internal capture stream for randshell’s AirPods Pro"
                node.name = "bluez_capture_internal.[MAC_HERE]"
                media.class = "Stream/Input/Audio/Internal"

[]...]                
        id 111, type PipeWire:Interface:Port/3
                object.serial = "5317"
                object.path = "bluez_output.[MAC_HERE].1:monitor_0"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FL"
                port.id = "0"
                port.name = "monitor_FL"
                port.direction = "out"
                port.monitor = "true"
                port.alias = "randshell’s AirPods Pro:monitor_FL"
                port.group = "stream.0"
[...]
        id 119, type PipeWire:Interface:Node/3
                object.serial = "5310"
                factory.id = "12"
                client.id = "42"
                device.id = "184"
                priority.session = "1010"
                priority.driver = "1010"
                node.description = "randshell’s AirPods Pro"
                node.name = "bluez_output.[MAC_HERE].1"
                media.class = "Audio/Sink"
[...]
        id 140, type PipeWire:Interface:Port/3
                object.serial = "3765"
                object.path = "bluez_capture_internal.[MAC_HERE]:monitor_0"
                format.dsp = "32 bit float mono audio"
                node.id = "82"
                audio.channel = "MONO"
                port.id = "0"
                port.name = "monitor_MONO"
                port.direction = "out"
                port.monitor = "true"
                port.alias = "Bluetooth internal capture stream for randshell’s AirPods Pro:monitor_MONO"
                port.group = "stream.0"
[...]
        id 169, type PipeWire:Interface:Port/3
                object.serial = "5315"
                object.path = "bluez_output.[MAC_HERE].1:playback_0"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FL"
                port.id = "0"
                port.name = "playback_FL"
                port.direction = "in"
                port.physical = "true"
                port.terminal = "true"
                port.alias = "randshell’s AirPods Pro:playback_FL"
                port.group = "stream.0"
[...]
        id 171, type PipeWire:Interface:Port/3
                object.serial = "3766"
                object.path = "bluez_input.[MAC_HERE]:capture_0"
                format.dsp = "32 bit float mono audio"
                node.id = "194"
                audio.channel = "MONO"
                port.id = "0"
                port.name = "capture_MONO"
                port.direction = "out"
                port.alias = "randshell’s AirPods Pro:capture_MONO"
                port.group = "stream.0"
[...]
        id 180, type PipeWire:Interface:Port/3
                object.serial = "5316"
                object.path = "bluez_output.[MAC_HERE].1:playback_1"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FR"
                port.id = "1"
                port.name = "playback_FR"
                port.direction = "in"
                port.physical = "true"
                port.terminal = "true"
                port.alias = "randshell’s AirPods Pro:playback_FR"
                port.group = "stream.0"
[...]
        id 184, type PipeWire:Interface:Device/3
                object.serial = "3755"
                factory.id = "15"
                client.id = "42"
                device.api = "bluez5"
                device.description = "randshell’s AirPods Pro"
                device.name = "bluez_card.[MAC_HERE]"
                media.class = "Audio/Device"
[...]
        id 194, type PipeWire:Interface:Node/3
                object.serial = "3762"
                factory.id = "12"
                client.id = "218"
                device.id = "184"
                priority.session = "2010"
                node.description = "randshell’s AirPods Pro"
                node.name = "bluez_input.[MAC_HERE]"
                media.class = "Audio/Source"
[...]
        id 223, type PipeWire:Interface:Port/3
                object.serial = "3764"
                object.path = "bluez_capture_internal.[MAC_HERE]:input_0"
                format.dsp = "32 bit float mono audio"
                node.id = "82"
                audio.channel = "MONO"
                port.id = "0"
                port.name = "input_MONO"
                port.direction = "in"
                port.alias = "Bluetooth internal capture stream for randshell’s AirPods Pro:input_MONO"
                port.group = "stream.0"
        id 224, type PipeWire:Interface:Port/3
                object.serial = "5318"
                object.path = "bluez_output.[MAC_HERE].1:monitor_1"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FR"
                port.id = "1"
                port.name = "monitor_FR"
                port.direction = "out"
                port.monitor = "true"
                port.alias = "randshell’s AirPods Pro:monitor_FR"
                port.group = "stream.0"

pw-top while recording something on AirPods_HiRes_Mic:

image

pw-top while playing something on AirPods_HiRes_Sink (without hearing anything):

image

@LuanAdemi

LuanAdemi commented Jul 14, 2026

Copy link
Copy Markdown
Author

@LuanAdemi I'm on the latest Fedora 44 Kinoite.

When I'm on AirPods_HiRes_Sink, I can't hear anything and I've noticed it's a mono sink. However the AirPods_HiRes_Mic records in high-quality, which I can only check when disabling the Hi-Res feature and going back to the normal sink playback.

Another thing I've noticed is that when I switch the playback from AirPods_HiRes_Sink to the normal one while the Hi-Res is on, I can hear the sounds but the microphone stream gets added too, so I hear both the playback and the real-time Hi-Res microphone. I'm not sure if this is a technical limitation, but maybe it helps with debugging the issue.

Below some extra info. Let me know what else you'd need.

`$ pw-cli ls`
[...]
        id 82, type PipeWire:Interface:Node/3
                object.serial = "3763"
                factory.id = "12"
                client.id = "218"
                node.description = "Bluetooth internal capture stream for randshell’s AirPods Pro"
                node.name = "bluez_capture_internal.[MAC_HERE]"
                media.class = "Stream/Input/Audio/Internal"

[]...]                
        id 111, type PipeWire:Interface:Port/3
                object.serial = "5317"
                object.path = "bluez_output.[MAC_HERE].1:monitor_0"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FL"
                port.id = "0"
                port.name = "monitor_FL"
                port.direction = "out"
                port.monitor = "true"
                port.alias = "randshell’s AirPods Pro:monitor_FL"
                port.group = "stream.0"
[...]
        id 119, type PipeWire:Interface:Node/3
                object.serial = "5310"
                factory.id = "12"
                client.id = "42"
                device.id = "184"
                priority.session = "1010"
                priority.driver = "1010"
                node.description = "randshell’s AirPods Pro"
                node.name = "bluez_output.[MAC_HERE].1"
                media.class = "Audio/Sink"
[...]
        id 140, type PipeWire:Interface:Port/3
                object.serial = "3765"
                object.path = "bluez_capture_internal.[MAC_HERE]:monitor_0"
                format.dsp = "32 bit float mono audio"
                node.id = "82"
                audio.channel = "MONO"
                port.id = "0"
                port.name = "monitor_MONO"
                port.direction = "out"
                port.monitor = "true"
                port.alias = "Bluetooth internal capture stream for randshell’s AirPods Pro:monitor_MONO"
                port.group = "stream.0"
[...]
        id 169, type PipeWire:Interface:Port/3
                object.serial = "5315"
                object.path = "bluez_output.[MAC_HERE].1:playback_0"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FL"
                port.id = "0"
                port.name = "playback_FL"
                port.direction = "in"
                port.physical = "true"
                port.terminal = "true"
                port.alias = "randshell’s AirPods Pro:playback_FL"
                port.group = "stream.0"
[...]
        id 171, type PipeWire:Interface:Port/3
                object.serial = "3766"
                object.path = "bluez_input.[MAC_HERE]:capture_0"
                format.dsp = "32 bit float mono audio"
                node.id = "194"
                audio.channel = "MONO"
                port.id = "0"
                port.name = "capture_MONO"
                port.direction = "out"
                port.alias = "randshell’s AirPods Pro:capture_MONO"
                port.group = "stream.0"
[...]
        id 180, type PipeWire:Interface:Port/3
                object.serial = "5316"
                object.path = "bluez_output.[MAC_HERE].1:playback_1"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FR"
                port.id = "1"
                port.name = "playback_FR"
                port.direction = "in"
                port.physical = "true"
                port.terminal = "true"
                port.alias = "randshell’s AirPods Pro:playback_FR"
                port.group = "stream.0"
[...]
        id 184, type PipeWire:Interface:Device/3
                object.serial = "3755"
                factory.id = "15"
                client.id = "42"
                device.api = "bluez5"
                device.description = "randshell’s AirPods Pro"
                device.name = "bluez_card.[MAC_HERE]"
                media.class = "Audio/Device"
[...]
        id 194, type PipeWire:Interface:Node/3
                object.serial = "3762"
                factory.id = "12"
                client.id = "218"
                device.id = "184"
                priority.session = "2010"
                node.description = "randshell’s AirPods Pro"
                node.name = "bluez_input.[MAC_HERE]"
                media.class = "Audio/Source"
[...]
        id 223, type PipeWire:Interface:Port/3
                object.serial = "3764"
                object.path = "bluez_capture_internal.[MAC_HERE]:input_0"
                format.dsp = "32 bit float mono audio"
                node.id = "82"
                audio.channel = "MONO"
                port.id = "0"
                port.name = "input_MONO"
                port.direction = "in"
                port.alias = "Bluetooth internal capture stream for randshell’s AirPods Pro:input_MONO"
                port.group = "stream.0"
        id 224, type PipeWire:Interface:Port/3
                object.serial = "5318"
                object.path = "bluez_output.[MAC_HERE].1:monitor_1"
                format.dsp = "32 bit float mono audio"
                node.id = "119"
                audio.channel = "FR"
                port.id = "1"
                port.name = "monitor_FR"
                port.direction = "out"
                port.monitor = "true"
                port.alias = "randshell’s AirPods Pro:monitor_FR"
                port.group = "stream.0"

pw-top while recording something on AirPods_HiRes_Mic:

image

pw-top while playing something on AirPods_HiRes_Sink (without hearing anything):

image

Oh. So you were using the sink as an audio output? This is a null sink my implementation needs to make some stuff easier. So if you had that sink selected as an output can you maybe try selecting the A2DP sink in the audio settings? The configuration should be: A2DP for the output, AirPods_HiRes_Mic for the input device. Just disregard that sink.

If this was the issue, I might consider dropping that null sink since it might be confusing.

@randshell

randshell commented Jul 14, 2026

Copy link
Copy Markdown

The configuration should be: A2DP for the output, AirPods_HiRes_Mic for the input device. Just disregard that sink.

I have two sinks available as output, the HiRes one which now I'm understanding is a null sink, and the AirPods. When selecting the AirPods, I hear both the desktop audio but also the audio from the microphones (with a slight delay).

Even if one of them is a null sink, the other one shouldn't get the microphone stream added together with the normal desktop output. At least, this is how it looks like when I try it. Fedora 44 Kinoite, AirPods Pro 3, commit 1f8948d.

Btw. This happens only when HiRes is enabled. Without it, the AirPods sink only streams the normal desktop output.

@LuanAdemi

LuanAdemi commented Jul 14, 2026

Copy link
Copy Markdown
Author

The configuration should be: A2DP for the output, AirPods_HiRes_Mic for the input device. Just disregard that sink.

I have two sinks available as output, the HiRes one which now I'm understanding is a null sink, and the AirPods. When selecting the AirPods, I hear both the desktop audio but also the audio from the microphones (with a slight delay).

Even if one of them is a null sink, the other one shouldn't get the microphone stream added together with the normal desktop output. At least, this is how it looks like when I try it. Fedora 44 Kinoite, AirPods Pro 3, commit 1f8948d.

Btw. This happens only when HiRes is enabled. Without it, the AirPods sink only streams the normal desktop output.

Got it. I had this issue before. I will try to reproduce this on the new commits. Thank you for reporting and therefore supporting this effort 🙂.

One more thing: Can you run pw-link -l while in that state you described and paste the output into this thread?
Make sure to redact your MAC addresses, if you don't want them published.

For me the whole pipewire graph when playing a youtube video while recording with audacity looks like this:

AirPodsHiRes_raw:monitor_MONO
  |-> input.AirPodsHiRes:input_MONO
AirPodsHiRes_raw:playback_MONO
  |<- LibrePods:output_MONO
AirPodsHiRes:capture_MONO
  |-> ALSA plug-in [audacity]:input_MONO
input.AirPodsHiRes:input_MONO
  |<- AirPodsHiRes_raw:monitor_MONO
Zen:output_FL
  |-> bluez_output.REDACTED:playback_FL
Zen:output_FR
  |-> bluez_output.REDACTED:playback_FR
ALSA plug-in [audacity]:input_MONO
  |<- AirPodsHiRes:capture_MONO
LibrePods:output_MONO
  |-> AirPodsHiRes_raw:playback_MONO
bluez_output.REDACTED:playback_FL
  |<- Zen:output_FL
bluez_output.REDACTED:playback_FR
  |<- Zen:output_FR

@LuanAdemi

LuanAdemi commented Jul 14, 2026

Copy link
Copy Markdown
Author

You can also try the new commit. It removes the null sink and pipes the mic stream through a named pipe directly to the virtual input. Previously I just created a null sink to pipe the raw audio PCMs into the PipeWire graph and used a virtual input that monitored that null sink and relays the audio to the input node. From what I can tell, there are only two ways your issue can happen. Either something is connecting the null sink to your bluez sink (1), or the input is piped into the output (2) (like a mic test in any video conference software does). The newest commit makes (1) impossible.

@randshell

Copy link
Copy Markdown

Thank you, I’ll try!

When it’s a file rather than a sink, can programs attach to it and read it? If the mute button doesn’t get respected along the path, it’d be an issue imo.

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.

3 participants