diff --git a/.typos.toml b/.typos.toml index 85be23aba..fb23ec535 100644 --- a/.typos.toml +++ b/.typos.toml @@ -26,6 +26,7 @@ facter = "facter" # nixos-facter-modules crypted = "crypted" # LUKS device mapper name wdth = "wdth" # CSS font-variation-settings property substituters = "substituters" # nix substituters +ANC = "ANC" # Active Noise Cancellation (AirPods) # Hashes/keys (extend-ignore-re should catch most, but explicit for safety) "981DE78A201C2B735FF0B545A3967CCA47D5275F" = "981DE78A201C2B735FF0B545A3967CCA47D5275F" diff --git a/modules/apps/librepods.nix b/modules/apps/librepods.nix index 8d8b4d1d6..26a994c99 100644 --- a/modules/apps/librepods.nix +++ b/modules/apps/librepods.nix @@ -37,6 +37,25 @@ let config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; + + # AirPods Max 2 (model A3454) outputs left-channel-only audio when the + # WirePlumber/BlueZ stack picks the non-standard SBC-XQ codec, which is + # selected by default on Linux when both peers advertise it. Reported + # at https://github.com/kavishdevar/librepods/pull/519#issuecomment-4230312279 + # by the PR author after testing on Arch with the same WirePlumber + # build NixOS ships. `bluez5.enable-sbc-xq` is a daemon-wide property + # in WirePlumber's BlueZ monitor, so this disables SBC-XQ for every + # bluetooth sink rather than just the Max 2; it is the same scope the + # upstream workaround uses. Other sinks negotiate plain SBC (or AAC if + # both ends support it), which is the codec all non-Max AirPods and + # most generic bluetooth speakers default to anyway. Drop this block + # if upstream librepods/WirePlumber gain a per-device opt-out, or if + # the Max 2 firmware ships a fix for the SBC-XQ stereo handling. + services.pipewire.wireplumber.extraConfig."51-disable-sbc-xq" = { + "monitor.bluez.properties" = { + "bluez5.enable-sbc-xq" = false; + }; + }; }; }; in diff --git a/modules/base/custom-packages-overlay.nix b/modules/base/custom-packages-overlay.nix index e545ff9d4..7af91bf84 100644 --- a/modules/base/custom-packages-overlay.nix +++ b/modules/base/custom-packages-overlay.nix @@ -46,7 +46,11 @@ _: { # qtquick3d for qtdeclarative + qttools and adds Widgets/DBus. # Dep lookups go through `final` so any later overlay (e.g. an `openssl` # CVE patch) feeds into this build instead of being silently bypassed. - librepods = prev.librepods.overrideAttrs (_old: rec { + # The Max-2 patch backports https://github.com/kavishdevar/librepods/pull/519 + # so AirPods Max 2 (BLE 0x2D20 / model A3454) is recognised; without it + # the device falls through to the unknown-model defaults. Drop the patch + # once upstream merges PR #519 and a release pinning it ships. + librepods = prev.librepods.overrideAttrs (old: rec { version = "0.2.5"; src = final.fetchFromGitHub { owner = "kavishdevar"; @@ -54,6 +58,9 @@ _: { tag = "v${version}"; hash = "sha256-6l1WjwjDbv5e3tDaWo9+XSEjr9ge/hKysIkeUqyiO4U="; }; + patches = (old.patches or [ ]) ++ [ + ../../packages/librepods/airpods-max-2.patch + ]; buildInputs = [ final.libpulseaudio final.openssl diff --git a/packages/librepods/airpods-max-2.patch b/packages/librepods/airpods-max-2.patch new file mode 100644 index 000000000..ef37aeaf8 --- /dev/null +++ b/packages/librepods/airpods-max-2.patch @@ -0,0 +1,78 @@ +Subject: [PATCH] add basic AirPods Max 2 support and stub max_case.png + +Two related fixes squashed into one patch: + +1. Backport https://github.com/kavishdevar/librepods/pull/519 onto v0.2.5 so + AirPods Max 2nd Gen (BLE 0x2D20, model A3454) is recognised. Without this + the device falls through to the Unknown enum branch. + +2. Replace the dangling `max_case.png` reference in `getModelIcon()` with the + existing `podmax.png` asset. Upstream never shipped `max_case.png`: it does + not exist on `main`, on `linux/rust`, or in the Android `res-apple` + drawables (which only carry case art for AirPods 1/2/3/4/Pro 1/2/3, never + Max). AirPods Max has no charging case, just a non-electronic Smart Case + sleeve, so there is no canonical Apple artwork for a "Max case battery". + The case PodColumn is hidden for Max anyway (`caseAvailable` stays false + because battery is reported via the Headset component), but QQuickImage + still resolves the source URL on creation, producing + `qrc:/icons/assets/max_case.png is not installed` warnings. Pointing both + slots at `podmax.png` silences the warning without inventing an asset and + stays semantically Max-shaped if the visibility logic ever changes. + +Patch paths are relative to the `linux/` build root because the nixpkgs +librepods derivation sets `sourceRoot = "source/linux"`. Upstream paths like +`linux/ble/blemanager.cpp` therefore become `ble/blemanager.cpp` here. The +upstream PR's `Proximity Pairing Message.md` docs change is omitted because +it lives outside the build root. + +Drop this patch once upstream PR #519 merges, a release pinning it ships, and +upstream stops referencing `max_case.png` (or actually adds the asset). + +diff --git a/ble/blemanager.cpp b/ble/blemanager.cpp +--- a/ble/blemanager.cpp ++++ b/ble/blemanager.cpp +@@ -16,6 +16,7 @@ AirpodsTrayApp::Enums::AirPodsModel getModelName(quint16 modelId) + {0x1B20, AirPodsModel::AirPods4ANC}, + {0x0A20, AirPodsModel::AirPodsMaxLightning}, + {0x1F20, AirPodsModel::AirPodsMaxUSBC}, ++ {0x2D20, AirPodsModel::AirPodsMax2}, + {0x0E20, AirPodsModel::AirPodsPro}, + {0x1420, AirPodsModel::AirPodsPro2Lightning}, + {0x2420, AirPodsModel::AirPodsPro2USBC} +diff --git a/enums.h b/enums.h +--- a/enums.h ++++ b/enums.h +@@ -32,6 +32,7 @@ namespace AirpodsTrayApp + AirPodsPro2USBC, + AirPodsMaxLightning, + AirPodsMaxUSBC, ++ AirPodsMax2, + AirPods4, + AirPods4ANC + }; +@@ -50,6 +51,7 @@ namespace AirpodsTrayApp + {"A2083", AirPodsModel::AirPodsPro}, + {"A2096", AirPodsModel::AirPodsMaxLightning}, + {"A3184", AirPodsModel::AirPodsMaxUSBC}, ++ {"A3454", AirPodsModel::AirPodsMax2}, + {"A2565", AirPodsModel::AirPods3}, + {"A2564", AirPodsModel::AirPods3}, + {"A3047", AirPodsModel::AirPodsPro2USBC}, +@@ -85,7 +87,8 @@ namespace AirpodsTrayApp + return {"podpro.png", "podpro_case.png"}; + case AirPodsModel::AirPodsMaxLightning: + case AirPodsModel::AirPodsMaxUSBC: +- return {"podmax.png", "max_case.png"}; ++ case AirPodsModel::AirPodsMax2: ++ return {"podmax.png", "podmax.png"}; + default: + return {"pod.png", "pod_case.png"}; // Default icon for unknown models + } +@@ -97,6 +100,7 @@ namespace AirpodsTrayApp + switch (model) { + case AirPodsModel::AirPodsMaxLightning: + case AirPodsModel::AirPodsMaxUSBC: ++ case AirPodsModel::AirPodsMax2: + return true; + default: + return false;