Skip to content

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

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

[Linux/rust] Implemented high-res microphone support#655
LuanAdemi wants to merge 13 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

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