Skip to content

Device: Add groundwork for AirPods Custom EQ - #689

Draft
d4rken wants to merge 1 commit into
mainfrom
feat/aap-custom-eq
Draft

Device: Add groundwork for AirPods Custom EQ#689
d4rken wants to merge 1 commit into
mainfrom
feat/aap-custom-eq

Conversation

@d4rken

@d4rken d4rken commented Aug 21, 2026

Copy link
Copy Markdown
Member

What changed

No user-facing behavior change. Adds AAP protocol support (decode and encode) for Apple's iOS 27 "Custom EQ", recovers two flags the existing Headphone Accommodations decoder was throwing away, and adds a debug-only control used to test the new opcode against real hardware.

Custom EQ does not currently work on any hardware available for testing. The fourth bullet below has the results.

Technical Context

  • The 0x63 wire format comes from a third-party project (librepods commit 7341e41) which never verified it either, so the decoder validates every field and returns null on any mismatch. The payload length check is a strict equality rather than a minimum, so a frame carrying trailing unknown bytes surfaces verbatim in the unknown-message hex log instead of being mis-parsed into plausible values. SetCustomEq deliberately gets no optimistic state update and no verification predicate: an unacknowledged write must not render as success, and no device is known to report this setting at all.

  • Opcode 0x53 is the Headphone Accommodations configuration, which the iOS 18.1+ hearing-aid feature reuses, rather than solely the hearing-aid audiogram as the previous comments claimed. Payload bytes 4 and 5 are its "Apply To: Phone / Media" flags, encoded as plain 0x01 flags and not the Apple-bool 0x01/0x02 used by the 0x09 control settings, so decodeAppleBool must not be used on them. They describe scope only and say nothing about whether a profile exists, so isAllZero stays a pure band-data predicate.

  • The debug card's Apply button is disabled while no pod is worn. AapOutboundController queues non-exempt commands in that state and the queue collapses repeats, so a write would otherwise surface minutes later at an unrelated moment and poison the log window the card exists to produce. Exempting SetCustomEq from ear-gating was considered and rejected: it would mutate shared production code for a debug tool, and it would allow EQ writes while the pods are unworn, which confounds the hardware test. The guard reads the AAP EarDetection setting rather than PodDevice.isEitherPodInEar, whose BLE fallback disagrees with the controller exactly when AAP ear data is absent.

  • On-device results, both legs negative. AirPods Pro 2 USB-C (A3048, firmware 81.2675000075000000.6814) and AirPods Pro 3 (A3064, firmware ...6503) never advertise 0x63 across connect bursts of 33 and 26 settings frames, never echo a write, never persist one across a case-cycle reconnect, and produce no audible change. Both accept the write at the socket and discard it. Worth noting the comparison is confounded: the Pro 3 is on older firmware than the Pro 2 on the same train, so this pair cannot separate a model line from a firmware line.

  • What that negative does not establish: that the byte format is wrong (a device ignoring an unimplemented opcode is indistinguishable from one rejecting a malformed frame), or that privileged access such as VendorID spoofing is required, since neither device advertises the feature at all. The cheapest decisive follow-up is checking whether Apple's own settings offer an Equalizer entry for these pods on an iOS 27 host.

Review checklist

  • 0x53 change is additive only: the 4x8 Float32 decode keeps offset 6, and isAllZero is unchanged.
  • Strict 0x63 decode is intentional. Any dialect mismatch must log verbatim rather than decode.
  • Debug card is src/main behind BuildConfig.DEBUG, mirroring the existing PME chart, with hardcoded English text so throwaway instrumentation labels never reach Crowdin.

Adds decode and encode for AAP opcode 0x63, Apple's iOS 27 "Custom EQ"
(three bands plus a Recommended/Custom mode selector, H2 models).

The wire format comes from librepods commit 7341e41 and has never been
confirmed on hardware, so the decoder validates every field and returns
null on any mismatch -- including a strict payload-length equality, so a
frame carrying trailing unknown bytes is surfaced verbatim in the
unknown-message hex log rather than mis-parsed into plausible values.
Nothing optimistically assumes the write lands: SetCustomEq deliberately
has no optimistic state update and no verification predicate, because an
unacknowledged write must not render as success, and the device is not
known to report this setting at all.

Also recovers two bytes the 0x53 decoder was discarding. Opcode 0x53 is
the Headphone Accommodations configuration -- an iOS Accessibility
feature that the iOS 18.1+ hearing-aid feature reuses -- and payload
bytes 4 and 5 are its "Apply To: Phone / Media" flags. They are plain
0x01 flags, not the Apple-bool 0x01/0x02 encoding used by the 0x09
control settings. They describe scope only and say nothing about whether
a profile exists, so isAllZero stays a pure band-data predicate.

A debug-only card (BuildConfig.DEBUG, mirroring the existing PME chart)
drives the opcode for hardware evaluation. It edits local draft state and
sends exactly one packet per Apply tap; slider-driven sends would flood
the link and make a logcat observation window unreadable. Apply is
disabled while no pod is worn, because AapOutboundController queues
non-exempt commands in that state and the queue collapses repeats,
surfacing them later at an unrelated moment. That guard reads the AAP
EarDetection setting rather than PodDevice's BLE-fallback property, so it
matches the controller's gate in every state -- notably, neither gates
when the AAP setting is absent.

On-device evaluation, both legs negative: AirPods Pro 2 USB-C (A3048,
fw 81.2675000075000000.6814) and AirPods Pro 3 (A3064, fw ...6503)
never advertise 0x63 across settings bursts of 33 and 26 frames, never
echo a write, never persist one across a case-cycle reconnect, and
produce no audible change. Both accept the write at the socket and
discard it. This does not establish that the byte format is wrong -- a
device ignoring an unimplemented opcode is indistinguishable from one
rejecting a malformed frame -- and it is not evidence that privileged
access is required, since neither device advertises the feature at all.
@d4rken d4rken added enhancement Add a new feature of improve an existing feature coms/AAP Uses Apples AirPod Protocol. Requires Android ROM with fixed L2CAP support on the Bluetooth sockets. labels Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coms/AAP Uses Apples AirPod Protocol. Requires Android ROM with fixed L2CAP support on the Bluetooth sockets. enhancement Add a new feature of improve an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant