Skip to content

XM4: Speak-to-Chat, multipoint and touch controls; menu-bar-only macOS app - #32

Open
JustinFB wants to merge 5 commits into
AmitRajput-Dev:mainfrom
JustinFB:macos-menubar-xm4-controls
Open

JustinFB wants to merge 5 commits into
AmitRajput-Dev:mainfrom
JustinFB:macos-menubar-xm4-controls

Conversation

@JustinFB

Copy link
Copy Markdown

Adds three settings to the WH-1000XM4 (v1 protocol): Speak-to-Chat, Connect to 2 devices and Touch Controls. Also turns the macOS app into a menu-bar-only app. Tested on a WH-1000XM4 on firmware 3.0.1, which macOS shows under a custom name.

XM4 protocol findings

These are taken from the headset's own replies and from an iPhone Sound Connect capture, recorded with Apple's Bluetooth logging profile and PacketLogger.

Speak-to-Chat was hidden on the XM4. The headset answers f6 05 with f7 05 00 <on>. It only uses the 01 kind byte (f9 05 01 <on>) in its change notifications. The probe required 01, so the setting never showed. It now accepts both. Gadgetbridge's V1 handler rejects this reply the same way.

Multipoint and touch panel are v1 general settings. They're found by name from the function list, using the layout from baglayan/acouplet#5:

Request Reply
Function list 06 00 07 00 <count> … (includes d1, d2)
Name d0 <slot> 01 d1 <slot> 02 <len>"MULTIPOINT_SETTING" … (or "TOUCH_PANEL_SETTING")
Value d6 <slot> d7 <slot> 01 <01 on / 00 off>
Change d8 <slot> 01 <01/00>

Turning multipoint on or off needs one extra step that isn't documented anywhere I could find. Without 94 01 00, the headset declines the change: it notifies d9 d2 01 01 (unchanged) and asks nothing. Sound Connect sends 94 01 00 once per connection. After it, the headset asks 99 01 07 01 (off) or 99 01 06 01 (on). Answering 98 01 <msg> 01 applies the change, and the headset restarts. This app sends 94 01 00 just before a multipoint change only, so other confirmation prompts that nothing here answers stay off.

All of this is in Headphones.cpp. tests/HeadphonesProtocolTests.cpp replays the captured replies. That includes the fake headset declining the change unless 94 01 00 came first, and a declined change being reported instead of shown as applied.

macOS app

This is the opinionated part; it's your call, and I'm happy to split it out or rework it.

  • Menu-bar only: no Dock icon and no window. The storyboard window scenes, ViewController and ContentView are gone, and the pieces the panel shares are now in SharedViews.swift.
    • Top of the panel: mode, ambient level, a draggable EQ graph (bands on a log-frequency axis with dB gridlines), the preset menu and Clear Bass.
    • Behind a footer arrow: settings, devices, appearance, device info and Quit.
  • Auto-connect on launch, and whenever a Sony headset connects to the Mac. It never goes through the device picker, and it stays off after a manual disconnect.
  • The menu-bar icon hides while no Sony headset is connected to the Mac. Relaunching the app shows it, with the panel.
  • Connect to 2 devices asks before restarting the headset, then reconnects once the headset is back.
  • Detection: a headset renamed in macOS is found by its Sony control service UUID, not just by name prefix.
  • Smaller changes: Native Glass follows the system light/dark setting in the panel; the codec chip is dropped; "Noise Cancelling" uses British spelling.

Testing

  • Client/tests/run-macos.sh passes: protocol, golden, metamorphic and connector-buffer tests.
  • Release build with Xcode 27: no errors.
  • On a WH-1000XM4 (fw 3.0.1):
    • connect without the device picker;
    • Speak-to-Chat shows and reads correctly;
    • Touch Controls toggles;
    • Connect to 2 devices goes off and back on (confirm, restart, auto-reconnect, reads back correctly);
    • auto-connect on launch;
    • icon hiding.
  • Not tested: v2 headsets, whose code paths are unchanged apart from the shared panel, and the Windows/Linux builds, which aren't touched.

🤖 Generated with Claude Code

JB-OD and others added 5 commits October 29, 2026 16:01
Auto-connect matched paired devices only by name prefix (WH-, WF-, ...), so a
headset renamed in macOS (e.g. "Birdie") fell through to the system device
picker, which doesn't list it. Also accept any paired device whose cached SDP
records advertise either Sony control service UUID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Run as a menu-bar app (LSUIElement): no Dock icon, and the window opens
  only from the menu-bar item (or by relaunching the app).
- Native Glass: set the menu-bar panel's appearance from the system
  light/dark setting; a status-item popover otherwise takes the menu bar's
  wallpaper-driven appearance.
- Menu-bar panel: collapsible "More Settings" with DSEE and Auto Power-Off.
- Equalizer: replace the preview and per-band sliders with a draggable graph.
  Bands sit on a log-frequency axis with dB gridlines; editing switches to
  Manual from the current preset's curve. Adds a Flat reset and a Clear Bass
  slider.
- Spell "Noise Cancelling" the British way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Captured from a WH-1000XM4 on fw 3.0.1, plus an iPhone Sound Connect
trace of the multipoint switch.

- Speak-to-Chat: the GET reply is f7 05 00 <on>, not f7 05 01 <on> (that
  kind only appears in change notifications), so the probe rejected it
  and the setting stayed hidden. Accept both.
- General settings: find MULTIPOINT_SETTING and TOUCH_PANEL_SETTING by
  name from the function list (06 00, d0 <slot> 01) and read them
  (d6 <slot> -> d7 <slot> 01 <01=on>), as acouplet does.
- Touch panel: d8 <slot> 01 <on/off>.
- Multipoint: send 94 01 00 first, as Sound Connect does; without it the
  headset declines with a d9 notification of the old value. Then
  d8 <slot> 01 <on/off>, and answer its 99 01 06/07 confirmation with
  98 01 06/07 01, after which it restarts. A declined change is read back
  and reported instead of being shown as applied.

Tests replay the captured replies, including the decline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Drop the window (storyboard scenes, ViewController, ContentView); its
  shared pieces move to SharedViews.swift. Everything lives in the
  menu-bar panel: the EQ graph, preset menu and Clear Bass up top, and
  settings, devices, appearance, device info and Quit behind a footer
  arrow. Drop the codec chip.
- Auto-connect on launch and whenever a Sony headset connects to the
  Mac, never via the device picker, and not after a manual disconnect.
- Hide the menu-bar icon while no Sony headset is connected to the Mac;
  relaunching the app shows it with the panel.
- XM4 switches: Connect to 2 devices (asks before restarting the
  headset, then reconnects once it's back) and Touch Controls. A
  declined change shows the headset's real state and an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workflow only triggered on master, which this repo doesn't have, so
it never ran. It would have failed anyway: upload-artifact@v3 is retired,
piping into xcpretty hid xcodebuild's exit status, and exporting the
archive needs a signing identity CI doesn't have.

Now: on main (and feature/** pushes, or by hand), run tests/run-macos.sh,
build the ad-hoc-signed Release app, and upload it zipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

2 participants