___ _ ___ __ __ __ __
/ | (_)____/ _ \____ / /_____ / // /__ / /___ ___ _____
/ /| |/ / ___/ /_)/ __ \/ __ / __|/ _ / _ \ / __ \/ _ \/ ___/
/ ___ / / / / ___/ /_/ / /_/ /\__ \ / / / __/ / /_/ / __/ /
/_/ |_/_/_/ /_/ \____/\__,_/|___/_/ /_/\___/_/ .___/\___/_/
/_/
Native Apple AirPods support for Linux — ANC control, transparency mode, battery levels, ear detection, parametric EQ, and more.
A lightweight Rust daemon communicates with AirPods over Bluetooth L2CAP using the Apple Accessory Protocol (AAP), exposes everything via D-Bus, and ships with a CLI tool and optional GTK4 desktop widgets.
| Device | Model Numbers | ANC | Adaptive | CA |
|---|---|---|---|---|
| AirPods 1 | A1523, A1722 | |||
| AirPods 2 | A2031, A2032 | |||
| AirPods 3 | A2564, A2565 | |||
| AirPods 4 | A3050, A3053, A3054, A3058 | |||
| AirPods 4 ANC | A3055, A3056, A3057, A3059 | ✓ | ✓ | ✓ |
| AirPods Pro | A2083, A2084, A2190 | ✓ | ||
| AirPods Pro 2 (Lightning) | A2698, A2699, A2700, A2931 | ✓ | ✓ | ✓ |
| AirPods Pro 2 (USB-C) | A2968, A3047, A3048, A3049 | ✓ | ✓ | ✓ |
| AirPods Pro 3 | A3063, A3064, A3065, A3122 | ✓ | ✓ | ✓ |
| AirPods Max | A2096 | ✓ | ||
| AirPods Max 2 | A3184 | ✓ | ✓ | ✓ |
The daemon auto-detects the connected model and exposes a Features D-Bus property so widgets and CLI only show controls your hardware supports.
- ANC modes — Off, Noise Cancellation, Transparency, Adaptive
- Battery levels — Left, Right, Case with charging status
- Ear detection — Auto-pause/resume media (MPRIS) when buds are removed/inserted
- Conversational Awareness — Enable/disable, with activity state tracking
- Adaptive noise level — Fine-tune noise cancellation intensity (0-100)
- One-bud ANC — ANC when wearing a single AirPod
- Microphone Mode — Select which bud is the primary mic (auto / left / right). See Microphone Mode & the A2DP+Mic Limitation for what this does and does not do.
- Parametric EQ — PipeWire filter-chain presets (flat, bass-boost, vocal-clarity, crinacle).
⚠️ Not currently functional — see Known Limitations. - Auto-reconnect — Exponential backoff reconnection on disconnect
- CLI tool — Full terminal control (
airpods-cli status,airpods-cli anc noise, etc.) - D-Bus interface —
org.costa.AirPodsfor integration with any desktop environment - GTK4 widgets — Bar button + popover + connection popup for AGS (Astal GTK Shell)
- Cross-platform desktop app — Tauri-based tray/settings UI under
app/(Linux today, Windows scaffolded)
The daemon communicates with AirPods using the Apple Accessory Protocol (AAP) — a proprietary binary protocol that Apple devices use over classic Bluetooth. The connection is established over an L2CAP channel on PSM 0x1001, bypassing the standard audio/HFP profiles to access device-level controls.
The protocol has been reverse-engineered by analyzing packet captures between AirPods and Apple devices. The daemon performs a multi-step handshake, then maintains a persistent connection where it sends commands (ANC mode changes, configuration) and receives unsolicited notifications (battery updates, ear detection events, firmware info).
All state is funneled into a shared watch channel and exposed over D-Bus, making the CLI and widgets simple, stateless clients.
Three install paths are supported on Linux. Pick whichever matches your distro.
All three paths run setcap during install so you don't need a separate sudo step. After install, run airpods-cli doctor to verify everything is wired up.
cd packaging
makepkg -si
systemctl --user enable --now airpods-daemon.service
airpods-cli doctorThe packaging/airpods-helper.install hook handles setcap automatically during the pacman transaction. AUR submission is pending.
./packaging/build-deb.sh
sudo dpkg -i packaging/airpods-helper_*_amd64.deb
systemctl --user enable --now airpods-daemon.service
airpods-cli doctorThe .deb's postinst runs setcap automatically.
# Requires: Rust toolchain, BlueZ dev headers (libdbus-1-dev / dbus-devel),
# PipeWire (for EQ), libcap (for setcap), and an active user systemd session
make install-system # builds, installs to /usr/local, runs setcap
systemctl --user enable --now airpods-daemon.service
airpods-cli doctormake install
sudo setcap 'cap_net_raw,cap_net_admin+eip' ~/.local/bin/airpods-daemon
systemctl --user enable --now airpods-daemon.service
airpods-cli doctorRuns end-to-end installation checks: daemon binary present, capabilities granted, BlueZ + PipeWire running, systemd unit active, D-Bus service reachable, AirPods currently connected. Each failed check prints a concrete fix command. Add --json for machine-readable output.
$ airpods-cli doctor
Checking airpods-helper installation...
✓ airpods-daemon binary — found at /usr/local/bin/airpods-daemon
✓ L2CAP raw socket capability — cap_net_raw + cap_net_admin set
✓ BlueZ system service — org.bluez reachable on system bus
✓ PipeWire — responding to `pw-cli info 0`
✓ Systemd user unit — airpods-daemon.service: active
✓ Daemon D-Bus service — org.costa.AirPods reachable
✓ AirPods connection — connected — AirPods Pro 2 (USB-C) (FW 6F8)
Everything looks good.
bluetoothctl
> scan on
> pair <MAC>
> trust <MAC>
> connect <MAC>The daemon auto-detects AirPods by service UUID and establishes the AAP connection.
airpods-cli status # full status display
airpods-cli status --json # JSON output for scripting
airpods-cli battery # battery levels onlyairpods-cli anc noise # noise cancellation
airpods-cli anc transparency # transparency mode
airpods-cli anc adaptive # adaptive mode
airpods-cli anc off # disable ANCairpods-cli ca on # enable conversational awareness
airpods-cli ca off # disable conversational awarenessPicks which bud's microphone is used when AirPods are routed as the input device (HFP/HSP mono call mode). See the explainer below — this does not enable concurrent stereo audio + mic.
airpods-cli mic auto # let the firmware pick (default)
airpods-cli mic left # force the left bud
airpods-cli mic right # force the right budairpods-cli eq list # list available presets
airpods-cli eq bass-boost # apply a preset
airpods-cli eq off # disable EQairpods-cli reconnect # trigger manual reconnect
airpods-cli doctor # diagnose installation health
airpods-cli doctor --json # JSON output for scripting┌─────────────────────────┐ D-Bus (org.costa.AirPods) ┌──────────────────┐
│ daemon (Rust) │ <-------------------------------> │ CLI / Widgets │
│ │ Properties + Signals + Methods │ │
│ BlueZ <- BT adapter │ │ airpods-cli │
│ L2CAP <- AAP protocol │ │ AirPodsBattery │
│ State <- watch channels │ │ AirPodsPopup │
│ EQ <- PipeWire │ └──────────────────┘
│ MPRIS <- ear detection │
└─────────────────────────┘
The daemon speaks AAP over L2CAP (PSM 0x1001) to the AirPods, maintaining a persistent connection. All state is exposed via D-Bus properties with PropertiesChanged signals for reactive UIs. The CLI and widgets are pure D-Bus clients.
Config file: ~/.config/airpods-helper/config.toml
[device]
# address = "AA:BB:CC:DD:EE:FF" # auto-detected if not set
[eq]
active_preset = "flat" # auto-loaded on connect
auto_load = true
[ear_detection]
pause_media = true # pause on removal
resume_media = true # resume on insertion
[reconnect]
auto_reconnect = true
max_retries = 3 # exponential backoff: 2s, 4s, 8sPresets are TOML files in ~/.config/airpods-helper/eq/:
| Preset | Description |
|---|---|
flat |
No EQ (passthrough) |
bass-boost |
Enhanced low-end |
vocal-clarity |
Mid-range emphasis |
airpods-pro-crinacle |
Crinacle's AirPods Pro target |
Custom presets: create a .toml file with name, description, preamp, and bands (type, freq, q, gain).
Service: org.costa.AirPods | Path: /org/costa/AirPods
| Property | Type | Description |
|---|---|---|
Connected |
b |
Device connected |
BatteryLeft |
i |
Left bud battery (0-100, -1 if unknown) |
BatteryRight |
i |
Right bud battery |
BatteryCase |
i |
Case battery |
ChargingLeft |
b |
Left bud charging |
ChargingRight |
b |
Right bud charging |
ChargingCase |
b |
Case charging |
AncMode |
s |
off, noise, transparency, adaptive |
EarLeft |
b |
Left bud in ear |
EarRight |
b |
Right bud in ear |
ConversationalAwareness |
b |
CA enabled |
ConversationalActivityState |
s |
normal, speaking, stopped |
AdaptiveNoiseLevel |
y |
0-100 |
OneBudAnc |
b |
Single-bud ANC |
Model |
s |
Device model identifier (e.g. airpods-pro-2-usb-c) |
ModelName |
s |
Human-readable model name |
Firmware |
s |
Firmware version |
Features |
as |
Capability list — subset of anc, adaptive, ca, one_bud_anc. Widgets and CLI use this to hide controls the connected hardware doesn't support. |
EqPreset |
s |
Active EQ preset name |
| Method | Args | Description |
|---|---|---|
SetAncMode |
(s) |
Set ANC mode |
SetConversationalAwareness |
(b) |
Toggle CA |
SetAdaptiveNoiseLevel |
(y) |
Set noise level 0-100 |
SetOneBudAnc |
(b) |
Toggle one-bud ANC |
SetMicMode |
(s) |
Primary mic bud: auto, left, or right |
SetEqPreset |
(s) |
Apply EQ preset by name |
DisableEq |
— | Remove EQ filter chain |
ListEqPresets |
— | List available preset names |
Reconnect |
— | Trigger device reconnect |
| Signal | Args | Description |
|---|---|---|
DeviceConnected |
(s) |
Model name |
DeviceDisconnected |
— | |
EarDetectionChanged |
(bb) |
Left, right in-ear |
The SetMicMode D-Bus method (and airpods-cli mic …) maps to AAP control sub-command 0x01 and selects which bud's microphone is treated as the primary input when AirPods are routed as the system input device:
auto— firmware decides based on ear detection, signal quality, etc. (default)left— pin the left bud's micright— pin the right bud's mic
This only matters when AirPods are negotiated as a mono call device (HFP/HSP / SCO / mSBC). It does not unlock simultaneous high-quality stereo audio + microphone input.
Classic Bluetooth has a hard profile mutex: the AirPods can speak A2DP (stereo audio out) or HFP/HSP (mono audio in + mono audio out), never both at once. The moment any app opens the AirPods as an input device, PipeWire/WirePlumber drops the link from A2DP to mSBC/SCO, and your music quality collapses to 8–16 kHz mono. This isn't a Linux bug — macOS hits the same brick wall. Mac users will notice that joining a FaceTime/Zoom call while playing music tanks the audio quality identically; Apple just hides the transition more gracefully.
The AirPods Pro 2 (H2 chip) and later are physically capable of full-duplex LE Audio (BAP/PACS over LC3), which would solve this — but Apple ships those endpoints gated behind Magic Pairing crypto. Specifically, AAP opcodes 0x30 / 0x31 perform an IRK / EncKey exchange against Apple's H2 device keys, and only after that does the AirPods advertise standard BAP service records. Without those keys, the AirPods refuse to expose LE Audio to a non-Apple host.
If you have the hardware, the patience, and ideally a USB BT sniffer or jailbroken iOS device, this is the single highest-impact contribution you could make to native AirPods support on Linux (and Android, and any non-Apple BT stack). What's needed:
- Packet captures of a clean Magic Pairing handshake — iOS device ↔ AirPods Pro 2/3 — covering opcodes
0x30and0x31end-to-end. - Key material analysis — figuring out whether Apple's IRK derivation is per-device-deterministic from public identifiers, or genuinely sealed inside the H2 / Secure Enclave.
- A handshake replay / impersonation PoC that gets a Linux host past the gate so the AirPods will advertise BAP/PACS.
Even partial wins (e.g. confirming the exact gating opcode pair on a specific firmware, or documenting failure modes) help. See LibrePods for the current state of community AAP research, and daemon/src/aap/mod.rs for the documented sub-command table this project uses. Open an issue if you want to coordinate before sinking time in.
- Parametric EQ is not currently working. The
airpods-cli eqcommands andSetEqPresetD-Bus method write a PipeWire filter-chain config and restart PipeWire, but the filter chain isn't actually taking effect on the AirPods output in practice. This is a known issue — the daemon plumbing is in place, the PipeWire integration needs fixing. Treat EQ as a work-in-progress; the preset files and config surface are stable. - No concurrent stereo audio + mic. See the section above — this is a Bluetooth-stack-level limitation, not specific to this project. Magic Pairing reverse-engineering is the path forward; contributions welcome.
- Windows daemon is out of sync. The
windows/crate is currently behind the Linux daemon on AAP coverage. Linux is the supported target today. - CI is currently red. A regression introduced around v0.2.0 needs cleanup. Local builds work; the GitHub Actions badge above does not reflect runtime quality.
The widget/ directory contains GTK4 widgets for AGS:
import { AirPodsBattery } from "./airpods"
// Add to your bar
const bar = new Gtk.Box()
bar.append(AirPodsBattery())Import widget/style.css in your AGS stylesheet for default dark theme styling.
Hard requirements (every feature except EQ):
- Linux with BlueZ 5.x
- D-Bus session + system bus (universal)
- Bluetooth adapter supporting BR/EDR (classic Bluetooth)
libcap(for the post-installsetcap)- AirPods Pro, AirPods Pro 2/3, AirPods Max, AirPods 3/4 (any model with AAP support)
Optional — only needed for parametric EQ:
- PipeWire + WirePlumber
EQ is the only feature that depends on PipeWire. Everything else — battery, ANC, ear detection (including MPRIS pause/resume), pair / quick-pair / connect, mic mode — runs on top of BlueZ + D-Bus only, so PulseAudio-only systems are supported (you just won't have the EQ feature, which is currently broken anyway).
| Distro | PipeWire default since | Status |
|---|---|---|
| Fedora | 34 (Apr 2021) | full support |
| Ubuntu | 22.10 (Oct 2022) | full support |
| Debian | 12 Bookworm (Jun 2023) | full support |
| Arch / Manjaro | rolling | full support (PipeWire is the standard pick) |
| openSUSE | Tumbleweed + Leap 15.5+ | full support |
| Pop!_OS / Mint | follow upstream | full support on modern releases |
| Ubuntu 20.04 LTS | PulseAudio only | works without EQ |
| Devuan / MX Linux | varies (often PulseAudio) | works without EQ |
The PKGBUILD lists pipewire and wireplumber as optdepends; the .deb lists them as Recommends. Neither is a hard install requirement.
Start with airpods-cli doctor — it runs all the checks below automatically and prints concrete fix commands for whatever is broken.
"Permission denied" on L2CAP connect:
sudo setcap 'cap_net_raw,cap_net_admin+eip' <path-to-airpods-daemon>The daemon logs this exact command on startup if the capability is missing.
Daemon not detecting AirPods:
- Ensure AirPods are paired and connected via
bluetoothctl - Check
journalctl --user -u airpods-daemon -ffor logs - Set
RUST_LOG=airpods_daemon=debugfor verbose output
EQ causes audio dropout: Applying/removing EQ restarts PipeWire to reload the filter chain config. This causes a brief audio interruption.
Widget not showing:
Ensure the widget directory is symlinked: ls -la ~/.config/ags/widget/airpods
Contributions are welcome! Here is how to get started:
- Fork the repository and create a feature branch
- Install dependencies: Rust toolchain +
libdbus-1-dev(ordbus-develon Fedora) - Build and test:
make build && cargo test --workspace - Run clippy:
cargo clippy --workspace -- -D warnings - Open a pull request against
main
- 🔓 Reverse-engineer Apple Magic Pairing (AAP 0x30 / 0x31). Unlocks LE Audio BAP/PACS on AirPods Pro 2 and later, which is the only known path to concurrent stereo audio + mic on Linux. See Microphone Mode & the A2DP+Mic Limitation for what specifically is needed — packet captures, key-derivation analysis, handshake replay PoC.
- Fix the parametric EQ pipeline. See Known Limitations — the daemon writes a PipeWire filter chain but it isn't taking effect. Anyone familiar with WirePlumber session policy /
pw-clidebugging would help. - Bring the Windows daemon back in sync. The
windows/crate has fallen behind on AAP coverage; CI has been red since v0.2.0. - Packet captures for new models / firmware. Especially AirPods Pro 3 and AirPods Max 2. Open an issue to coordinate before sinking time in.
MIT