Sony Head Tracker supports macOS 14 or later through a native IOHID backend, a command-line bridge, and a SwiftUI application. The implementation reuses the same platform-independent descriptor parser, orientation filter, recentering, axis mapping, and UDP serializers as the Windows application.
This is an unofficial project and is not affiliated with or endorsed by Sony.
Device discovery is based on the Android Head Tracker HID protocol, not a list of Sony product IDs. A usable device must expose:
- top-level usage page
0x20and usage0xE1; - a compatible
#AndroidHeadTracker#descriptor marker; - descriptor-defined input and feature elements that can be read through IOHID.
The macOS implementation does not hardcode model names, VID/PID values, report IDs, report lengths, or feature report bytes. Therefore WH/WF-1000XM5, WH/WF-1000XM6, ULT WEAR, and future models exposing the same protocol all pass through the same discovery and parsing code.
Platform-specific validation is deliberately reported separately from expected protocol compatibility:
| Model | macOS status |
|---|---|
| Sony ULT WEAR (WH-ULT900N) | Hardware validated |
| Sony WH-1000XM5 | Protocol-compatible; macOS hardware confirmation requested |
| Sony WF-1000XM5 | Protocol-compatible; macOS hardware confirmation requested |
| Sony WH-1000XM6 | Protocol-compatible; macOS hardware confirmation requested |
| Sony WF-1000XM6 | Protocol-compatible; macOS hardware confirmation requested |
Other models can be checked without changing the source code by running
probe. A verified collection is the compatibility requirement.
Each tagged release includes sony-head-tracker-vX.Y.Z-macos-universal.zip
with the SwiftUI app, the CLI bridge, and documentation. The binaries are
universal (Apple Silicon and Intel) and require macOS 14 or later. Xcode and
CMake are not required to use a downloaded release.
The app and CLI are ad-hoc signed but not notarized, so Gatekeeper flags the first launch:
- Unzip the archive and move
SonyHeadTracker.appwherever you like, for example/Applications. - Right-click (Control-click) the app and choose Open, then confirm. If
macOS does not offer Open, go to System Settings > Privacy &
Security, scroll to the blocked-app notice, and click Open Anyway.
Alternatively, clear the quarantine attribute from Terminal:
xattr -dr com.apple.quarantine SonyHeadTracker.app - Grant Input Monitoring permission as described under Permissions.
The bundled CLI runs directly from Terminal, for example
./sony-head-tracker-macos probe; the same one-time Gatekeeper approval (or
xattr -d com.apple.quarantine sony-head-tracker-macos) applies.
Verify a download against the release's SHA256SUMS.txt, and its
build-provenance attestation with
gh attestation verify <zip> --repo NicholasSlattery/sony-head-tracker.
- macOS 14 or later;
- full Xcode selected with
xcode-select; - CMake 3.25 or later;
- XcodeGen only when regenerating the committed SwiftUI Xcode project;
- a paired compatible headset with current firmware.
Verify the toolchain:
xcode-select -p
xcrun --show-sdk-path
clang++ --version
cmake --version
sw_vers
uname -mIf full Xcode is installed but not selected:
sudo xcode-select --switch /Applications/Xcode.app/Contents/DeveloperThis changes the active developer toolchain used by terminal builds. It does not remove, reset, or otherwise prevent normal use of Xcode.
The app and CLI need permission to listen to IOHID input reports. On first use, allow the executable under:
System Settings > Privacy & Security > Input Monitoring
Restart the app or CLI after changing the permission. The macOS app also includes a Bluetooth usage description because its recovery path uses public IOBluetooth APIs to reconnect the same previously verified paired headset and refresh its SDP services after a power cycle.
An ad-hoc signed Debug build has a designated requirement based on that build's
code hash. macOS can therefore treat the next build as a different executable
even though System Settings still shows an older SonyHeadTracker entry as
enabled. The committed build_and_run.sh does not inspect the Keychain or sign
anything by default. To opt in, set SHT_ENABLE_STABLE_SIGNING=1; the script
then detects exactly one valid Apple Development identity in the login keychain
and re-signs the bundle with that stable identity before launch.
No certificate name, hash, Team ID, or account information is stored in the
repository.
If more than one Apple Development identity is installed, choose one explicitly:
SHT_ENABLE_STABLE_SIGNING=1 \
SHT_CODE_SIGN_IDENTITY="certificate SHA-1 hash or exact name" \
./script/build_and_run.shWithout SHT_ENABLE_STABLE_SIGNING=1, the script retains Xcode's local ad-hoc
signature. When no suitable identity exists, rebuilding can require removing the stale App entry
from Input Monitoring, adding the newly built App, enabling it, and restarting
the App. Building directly in Xcode can instead use a development team selected
under the target's Signing & Capabilities settings.
The first macOS version intentionally does not enable App Sandbox while the IOHID hardware path is being validated. UDP output is restricted to loopback.
cmake -S . -B build/macos -DCMAKE_BUILD_TYPE=Release
cmake --build build/macos --parallel
ctest --test-dir build/macos --output-on-failureThe CLI executable is:
build/macos/sony-head-tracker-macos
Supported commands:
sony-head-tracker-macos probe
sony-head-tracker-macos dump [--seconds N]
sony-head-tracker-macos bridge [--port 4242] [--seconds N]
[--axis-map YXZ]
[--invert XZ]
[--smoothing 0.18]
sony-head-tracker-macos help | version
probe lists candidate HID collections and verifies the Android Head Tracker
descriptor. dump prints decoded samples for hardware diagnosis. bridge
streams full-rate data to OpenTrack on UDP 127.0.0.1:4242 and version 2 JSON on
UDP 127.0.0.1:4243 by default. The binary OpenTrack packet remains exactly six
little-endian doubles (x, y, z, yaw, pitch, roll), with translation set to
zero. Send SIGUSR1 to recenter the running CLI process.
The Xcode project is committed and can be built directly. After changing
macos/project.yml, regenerate it from the committed XcodeGen specification:
# Optional after project.yml changes: (cd macos && xcodegen generate)
./script/build_and_run.shThe build script uses a stable DerivedData directory under build/DerivedData,
builds the Debug app, optionally applies a stable local development signature
only when SHT_ENABLE_STABLE_SIGNING=1 is set, and launches a new instance. It accepts --debug, --logs,
--telemetry, and --verify for development diagnostics.
The app displays connection state, the selected device, yaw/pitch/roll, gyroscope values, sample rate, a bounded orientation graph, loopback output ports, device details, persistent filter/port settings, and actionable permission or connection errors. Its shareable diagnostic snapshot reports the app/OS/architecture, IOHID access, candidate and element counts, sanitized descriptor and feature read-back, packet/sample/UDP health, reconnect attempts, settings, and a classified last error without including a Bluetooth address or personalized device name. Hardware callbacks are copied to value types and delivered to the MainActor; UDP continues at the headset's full rate while visible UI updates are limited to about 30 Hz.
On the tested ULT WEAR, macOS exposes and configures the Android Head Tracker HID collection but does not begin sending changing input reports until the Bluetooth A2DP audio path is active. Manual music playback proves the sensor works, but is not suitable as an application requirement.
After configuring a verified tracker, the macOS implementation therefore starts a zero-filled PCM stream on the Core Audio output that belongs to that exact verified Bluetooth headset. The stream:
- is silent and does not change the system output device or volume;
- is selected by the verified device address, with an unambiguous exact product name only as a fallback;
- remains active for the tracking session because stopping it can stop the HID samples;
- stops when tracking stops, the device disconnects, recovery begins, or the process exits;
- may cause a modest increase in headset battery use while tracking.
This keepalive is not keyed to the text ULT WEAR, a Sony VID/PID, or a report
layout. Other verified Android Head Tracker devices use the same generic path;
their model-specific macOS behavior still needs hardware confirmation. ULT WEAR
is called out because it is the model on which the requirement was observed and
hardware-validated.
After a headset is verified, its Bluetooth address is stored with mode 0600 in:
~/Library/Application Support/SonyHeadTracker/last-tracker.plist
The address is used only to recover that same paired device after it is powered off and on. If its Android Head Tracker collection is absent, recovery first asks the paired device to connect and refresh SDP. A later retry may perform one baseband close/open cycle followed by another SDP refresh. The code does not scan for new devices, pair or forget devices, alter Bluetooth identities, open a private HID transport, or act on a different device. Addresses are not printed or included in UDP output.
Some ULT WEAR sessions expose the collection before samples resume. The CLI can perform one clean child-process recovery rather than looping indefinitely. The SwiftUI engine treats a configured connection with no valid sample for five seconds as a stalled stream: it closes the current silent-audio and IOHID session, refreshes SDP once, and retries after a separate short 1- then 2-second backoff. Later stalls recycle only IOHID and silent audio; stream timeout never closes the headset's Bluetooth baseband connection. Recovery state is cleared only after a valid sample arrives. During the separate 1, 2, 5, 10, and 30-second device-availability backoff, the App checks the exact paired headset and matching IOHID collection every 250 milliseconds. A Bluetooth reconnect or newly visible tracker collection wakes the retry immediately instead of waiting for the remaining backoff interval.
| Area | Windows | macOS |
|---|---|---|
| HID access | SetupAPI, HidD/HidP, Sensor API fallback | IOHIDManager, IOHIDDevice, CFRunLoop |
| UDP | Winsock | POSIX/BSD sockets |
| Bluetooth recovery | SetupAPI/device rebind and generic HID repair | Address-scoped public IOBluetooth connection/SDP refresh |
| UI | Win32/GDI | SwiftUI using a narrow C ABI over the C++ engine |
| Configuration path | %LOCALAPPDATA% |
~/Library/Application Support/SonyHeadTracker/ |
| Permissions | Windows device/UAC flow | Input Monitoring plus Bluetooth usage description |
| ULT WEAR activation | Normal Windows audio/device behavior | Session-lifetime silent Core Audio A2DP keepalive |
The Windows-only repair, bluetooth-rebind, bluetooth-generic-hid, Sensor
API fallback, Win32 GUI, and executable resources are not presented as macOS
features. Both platforms preserve the same filtering behavior and loopback UDP
wire formats.
- Confirm the headset is connected in System Settings > Bluetooth.
- Run
sony-head-tracker-macos probe. - Confirm Input Monitoring permission and restart the executable.
- Update the headset firmware using Sony Sound Connect.
- Temporarily disconnect phones or other multipoint hosts that may own the sensor transport.
- Power-cycle the headset and allow automatic paired-device recovery to run.
Removing and pairing the device again can refresh macOS's HID services, but it should be a last diagnostic step rather than a normal startup requirement.
Allow several seconds for the silent A2DP keepalive to activate the audio path.
The log should report the keepalive and then the first valid sample. If no sample
arrives, the App restarts the IOHID and silent-audio sessions after a short
backoff without deliberately disconnecting Bluetooth. Confirm that macOS still
lists the headset as connected and rerun probe if retries continue.
Only one process can bind a given local UDP receiver port. Start the bridge first,
then configure OpenTrack for UDP input port 4242. A JSON receiver must listen
on 127.0.0.1:4243. If --port N is used, JSON always uses N + 1.
The ULT WEAR validation completed the following checks on Apple Silicon:
- descriptor verification at usage
0x20:0xE1; - changing rotation and gyroscope samples at approximately 50 to 60 packets per second;
- recentering and reconnect after a headset power cycle;
- 48-byte OpenTrack packets with zero translation;
- version 2 JSON telemetry;
- 1,134 UDP samples in a 20-second post-review run with the silent A2DP keepalive active, including recovery from an initially absent HID collection.
Reports for XM5, XM6, and other descriptor-compatible models are welcome. Include
the model, firmware, macOS version, Mac architecture, and redacted probe output.