Skip to content

Hearing assistance on Windows (AirPods Pro 3) — ATT/GATT unblocked, feature end-to-end - #8

Closed
arctumn wants to merge 3 commits into
cross-platformfrom
hearing-assistant
Closed

Hearing assistance on Windows (AirPods Pro 3) — ATT/GATT unblocked, feature end-to-end#8
arctumn wants to merge 3 commits into
cross-platformfrom
hearing-assistant

Conversation

@arctumn

@arctumn arctumn commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Brings the AirPods Pro 3 hearing assistance (accessibility amplification) to the Windows client, end-to-end. Ready for review; one tuning step (the audiogram curve) is left for a follow-up.

The unlock

The hearing-aid audiogram lives on the AirPods' ATT/GATT (classic-Bluetooth L2CAP PSM 0x001F, handle 0x2A). The buds open that channel inbound to the host, which bthport refuses ("PSM not supported") — and a profile driver cannot register an L2CAP server on the reserved ATT PSM (BRB_L2CA_REGISTER_SERVERSTATUS_INVALID_PARAMETER).

The fix, matching Android's ATTManager: open the channel as a CLIENT (BRB_L2CA_OPEN_CHANNEL to PSM 0x001F — the same outbound path we already use for AAP 0x1001). Connecting as a client to a reserved PSM is allowed; the buds accept it and we then speak ATT.

What works (confirmed on hardware)

  • ATT channel opens (client) once the AAP link is up.
  • ATT read/write of the settings characteristic (handle 0x2A): CCCD notify-enable returns a Write Response (0x13); reading returns the 104-byte settings buffer; writing returns 0x13 (write=ok).
  • Audible amplification: enabling switches noise control to Transparency and writes a flat broadband audiogram; ambient sound is amplified and the tonal balance is clearly EQ-controllable (full audiogram control proven).
  • UI: Controls/HearingAidCard — on/off + amplification/balance sliders + conversation-boost (debounced), wired into DevicePage; localized in all four languages.

Architecture

  • Driver (crossplatform/windows/drivers/aap/): LpConnectAtt (client open), LpAttSend/LpAttReceive (BRB_L2CA_ACL_TRANSFER on the ATT channel) via IOCTL_LP_ATT_SEND/RECEIVE, LpCloseAttChannel, and ATT diagnostics through IOCTL_LP_GET_STATUS (DebugView never surfaced the driver's KdPrint, so status is read back in user mode). The dead server-register path is kept for reference.
  • Daemon (daemon/src/hearing.rs): Command::SetHearingAid { on, amplification, balance, conversation_boost } → enable hearing-assist over AAP (0x2C/0x33) → Transparency → ATT read-modify-write of handle 0x2A (per-ear amplification from amplification+balance, a flat EQ audiogram, conversation-boost).
  • IPC / WinUI: new command + SetHearingAidCmd DTO + the card.

Left for follow-up

  • Tune the audiogram curve. A flat BAND_GAIN = 30 sounds bass-heavy; a natural hearing-aid curve (high-frequency emphasis) needs a couple of hardware iterations.
  • Optional: a real per-band audiogram UI; make the auto-Transparency switch opt-out.

Not in scope

arctumn added 3 commits August 10, 2026 19:25
…ts GATT

The hearing-aid audiogram lives on the AirPods' ATT/GATT (PSM 0x001F). The buds open
that channel INBOUND to the host, which bthport refuses ('PSM not supported') — and a
profile driver CANNOT register an L2CAP server on the reserved ATT PSM (BRB_L2CA_
REGISTER_SERVER returns STATUS_INVALID_PARAMETER 0xC000000D). The fix, matching Android's
ATTManager: OPEN the channel as a CLIENT (BRB_L2CA_OPEN_CHANNEL to PSM 0x001F) — the same
outbound path we use for AAP 0x1001. Connecting as a client to a reserved PSM is allowed;
the buds accept it and we then speak ATT.

Driver: LpConnectAtt (client open, called from LpConnect once the AAP link is up),
LpAttSend/LpAttReceive (BRB_L2CA_ACL_TRANSFER on the ATT channel) exposed via
IOCTL_LP_ATT_SEND/RECEIVE, LpCloseAttChannel on teardown, and ATT diagnostics surfaced
through IOCTL_LP_GET_STATUS (register/accept/channel-open status) since DebugView never
showed our KdPrint. The unused server-register/accept path is kept for reference.

Daemon: driver.rs att_send/att_recv/att_diag; run_receiver logs the ATT state to
daemon.log. Proven end-to-end: enabling hearing-assist (0x2C/0x33) then ATT-reading
handle 0x2A returns the 104-byte hearing-aid settings buffer (ATT Read Response 0x0B),
and CCCD notif-enable (0x2B) returns a Write Response (0x13). GATT discovery shows two
custom Apple services — no standard Heart Rate service, consistent with HR being AAP-only.

NB the current att_probe auto-enables hearing-assist + probes/discovers on every connect
— that is test scaffolding; the shipped feature needs a real SetHearingAid IPC command
and the audiogram write. Head-tracking (AAP 0x17, per upstream issue librepods-org#713) is a separate
promising lead, untested here.
Turn the proven ATT read/write into a real feature. New IPC command
Command::SetHearingAid { on, amplification, balance, conversation_boost }; daemon
hearing.rs applies it — enables hearing-assist over AAP (0x2C/0x33) then read-
modify-writes the ATT/GATT settings characteristic (handle 0x2A): patches per-ear
amplification (from amplification + balance), tone and conversation-boost as LE
f32 at the Android offsets, leaving the audiogram EQ bands untouched. Runs on its
own thread (the enable settle + ATT round-trips take >1 s). The connect-time
att_probe scaffolding is removed; the ATT status logging stays.

WinUI: Controls/HearingAidCard (on/off toggle + amplification/balance sliders +
conversation-boost, debounced), wired into DevicePage and DaemonClient.SetHearingAid;
localized in all four languages. Slider values map 0..100 -> 0..1 amplification and
-100..100 -> -1..1 balance.

Not yet hardware-validated (the ATT WRITE — only the read was proven; the AirPods
kept dropping late in the session); the enable + read path is confirmed working.
…fication is audible

The ATT write succeeded but nothing was audible: the settings were written with a
zero audiogram (nothing for the amplification to scale) and the buds weren't passing
ambient. Now on enable we (1) switch noise control to Transparency (mode 3) so ambient
sound comes through, and (2) synthesize a flat broadband audiogram across all 8 EQ
bands per ear from the amplification slider. Confirmed audible on hardware — ambient
noise is amplified and the tonal balance is clearly EQ-controllable (proving full
audiogram control). BAND_GAIN (30) and the flat curve are first guesses; a natural
hearing-aid curve (high-frequency emphasis) is the tuning left for next session.
@arctumn

arctumn commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Superseded — the hearing-assistant work is folded into the native Windows stack in librepods-org#716. Closing.

@arctumn arctumn closed this Aug 11, 2026
@arctumn
arctumn deleted the hearing-assistant branch August 11, 2026 13:17
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