Skip to content

add basic airpods max 2 support (linux only)#519

Open
sachin3796 wants to merge 5 commits intokavishdevar:mainfrom
sachin3796:feature/airpods-max-2-support
Open

add basic airpods max 2 support (linux only)#519
sachin3796 wants to merge 5 commits intokavishdevar:mainfrom
sachin3796:feature/airpods-max-2-support

Conversation

@sachin3796
Copy link
Copy Markdown

Airpods Max 2 support

@sachin3796
Copy link
Copy Markdown
Author

Great project by the way!

Tested the basic application.

Screenshot of Airpods Max 2 in use:

image

Log from command line tool (censored MAC addresses and the Cloud Keys):

sach@arch-desktop ~/D/P/l/l/build (feature/airpods-max-2-support)> ./librepods
qt.bluetooth.bluez: Missing CAP_NET_ADMIN permission. Cannot determine whether a found address is of random or public type.
librepods:  Initializing LibrePods
librepods:  PulseAudio controller initialized
librepods:  Set ear detection behavior to:  MediaController::PauseWhenBothRemoved
librepods:  Connecting to device:  "Sach’s AirPods Max"
librepods:  AirPodsTrayApp initialized
librepods:  Noise control mode is already set to:  2
qt.qpa.services: Failed to register with host portal QDBusError("org.freedesktop.portal.Error.Failed", "Could not register app ID: App info not found for 'me.kavishdevar.librepods'")
librepods:  Connected to device, sending initial packets
qrc:/linux/PodColumn.qml:24:5: QML QQuickImage: Cannot open: qrc:/icons/assets/max_case.png
librepods:  Parsed AirPods metadata:
librepods:  Device Name:  "Sach’s AirPods Max"
librepods:  Model Number:  "A3454"
librepods:  Manufacturer:  "Apple Inc."
librepods:  Device output name set to:  "bluez_card.XX_XX_XX_XX_XX_XX"
librepods:  Noise control mode is already set to:  1
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Conversational awareness state received:  false
librepods:  One Bud ANC mode received:  false
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Primary Pod: Battery::Component::Headset
librepods:  Battery status:  "Headset: 31%"
librepods:  At least one AirPod is in ear
librepods:  Selected best available A2DP profile:  "a2dp-sink-sbc_xq"
librepods:  Activating A2DP profile for AirPods:  "a2dp-sink-sbc_xq"
librepods:  A2DP profile activated successfully
librepods:  Received Magic Cloud Keys:
librepods:  MagicAccIRK:  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
librepods:  MagicAccEncKey:  "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
librepods:  Device output name set to:  "bluez_card.XX_XX_XX_XX_XX_XX"
librepods:  Selected best available A2DP profile:  "a2dp-sink-sbc_xq"
librepods:  Activating A2DP profile for AirPods:  "a2dp-sink-sbc_xq"
librepods:  A2DP profile activated successfully
librepods:  A2DP profile activation attempted for newly connected device
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Adaptive
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Adaptive
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Off
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Off
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Adaptive
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Adaptive
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Transparency
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::NoiseCancellation
librepods:  Setting noise control mode to:  AirpodsTrayApp::Enums::NoiseControlMode::Off
librepods:  Noise control mode received:  AirpodsTrayApp::Enums::NoiseControlMode::Off

Can confirm that the modes are operating as expected.

I am not able to confirm the BLE modelId is correct at the moment, but I'm pretty sure I extracted it correctly by forcing the headphones in to BLE mode via bluetoothctl and dumping the bluetooth header with btmon.

@sachin3796
Copy link
Copy Markdown
Author

have now confirmed that the BLE modelId is now correct, and have updated the documentation with the correct value (0x2D20)

@sachin3796 sachin3796 changed the title add basic airpods max 2 support add basic airpods max 2 support (linux only) Apr 11, 2026
@sachin3796
Copy link
Copy Markdown
Author

As a note to add, I've had issues with the default selected codec "SBC-XQ" - if this codec is used, audio from the computer only comes out of the left speaker channel. This is likely a hardware limitation.

I've worked around this by adding the following to my wireplumber configuration:

$ cat .config/wireplumber/wireplumber.conf.d/12-disable-sbc-xq-profile.conf
monitor.bluez.properties = {
    bluez5.enable-sbc-xq = false
}

Both the bluetooth default SBC codec and AAC work correctly.

@kavishdevar kavishdevar force-pushed the main branch 6 times, most recently from 4bbaa29 to cb246d1 Compare April 26, 2026 12:06
Bad3r added a commit to Bad3r/nixos that referenced this pull request May 5, 2026
* feat(librepods): backport PR #519 for AirPods Max 2 support

Recognise AirPods Max 2nd Gen (BLE id 0x2D20, model A3454) so the Linux
Qt client gives it the Max icon and the single-headset battery layout
instead of falling through to the unknown-model defaults. The patch is
the squashed end-state diff of upstream PR kavishdevar/librepods#519,
with paths rewritten relative to `linux/` (the nixpkgs derivation's
`sourceRoot`) and the docs-only `Proximity Pairing Message.md` change
omitted. Drop the patch once upstream merges PR #519 and a release
shipping it is pinned.

Allow `ANC` (Active Noise Cancellation) in `.typos.toml` so the patch
file's `AirPods4ANC` enum value does not trip the spell-check hook.

Validation:
- nix flake check --accept-flake-config --no-build --offline
- nix build .#nixosConfigurations.system76.pkgs.librepods
- strings $out/bin/.librepods-wrapped | rg 'AirPodsMax2|A3454' -> both present

* fix(librepods): stub max_case.png and disable SBC-XQ for AirPods Max 2

Two AirPods Max follow-ups on top of the PR #519 backport:

1. Stop the QQuickImage warning about `qrc:/icons/assets/max_case.png is
   not installed`. Upstream librepods never shipped the asset and there
   is no canonical Apple artwork for it (AirPods Max has no charging
   case, just a non-electronic Smart Case sleeve), so the case
   PodColumn is hidden anyway via `caseAvailable`. The QML engine still
   resolves the dangling `iconSource` URL on creation, hence the
   warning. Point the case slot at the existing `podmax.png` so the
   image resolves; the column stays hidden in normal operation but no
   longer trips the loader.

2. Disable WirePlumber's SBC-XQ codec for all bluetooth sinks. The
   AirPods Max 2 reporter on PR #519 traced left-channel-only audio to
   SBC-XQ negotiation; the same workaround is documented there. The
   property `bluez5.enable-sbc-xq` is daemon-wide in WirePlumber's
   bluez monitor, so this matches the upstream workaround's scope.
   Other sinks fall back to plain SBC (or AAC where supported), which
   is the codec all non-Max AirPods and most generic bluetooth speakers
   default to.

Validation:
- nix flake check --accept-flake-config --no-build --offline -> all checks passed
- nix build .#nixosConfigurations.system76.pkgs.librepods -> rebuilt
- strings $out/bin/.librepods-wrapped | rg max_case -> no matches (fixed)
- strings $out/bin/.librepods-wrapped | rg 'AirPodsMax2|A3454' -> 2 matches (preserved)

* docs(librepods): point SBC-XQ note to specific upstream comment

The truncated `#issuecomment` URL fragment lacked the comment id, so
clicking it scrolled to the top of PR #519 instead of the workaround
report. Pin to issuecomment-4230312279 (the comment that documents the
left-channel-only audio bug and `bluez5.enable-sbc-xq = false` fix).

* docs(librepods): clarify path-rooting note in patch header

"Paths are rooted at `linux/`" was ambiguous: the patch entries do not
actually contain a `linux/` prefix. Reword to make explicit that paths
are relative to the `linux/` build root selected by `sourceRoot`, and
spell out the upstream-to-patch path mapping so a future maintainer
diffing this against PR #519 can see why the prefix is stripped.
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.

1 participant