Skip to content

Latest commit

 

History

97 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BudsCtl app icon

BudsCtl

Noise-cancellation control for your earbuds on macOS. AirPods menu bar experience, for buds that never got one.

MacOS gives AirPods a proper noise-mode control in the menu bar. Everyone else gets the touch gesture on the bud: tap and hold, guess which mode you landed on, try again. BudsCtl puts Off · Noise Cancellation · Transparency in your menu bar, in Control Center, in Shortcuts, and on a global hotkey with battery for each bud.

Apple Silicon · macOS 26+ · lives in the menu bar

GitHub downloads


The BudsCtl menu bar panel showing mode and battery

Supported earbuds

Verified Should work How
SoundPEATS Air4 Pro other SoundPEATS models Qualcomm GAIA V2 over Bluetooth LE
Samsung Galaxy Buds none Buds FE, Buds Pro, Buds2, Buds2 Pro, Buds3, Buds3 Pro, Buds3 FE, Buds Core, Buds4, Buds4 Pro Samsung SPP over Bluetooth Classic RFCOMM

Verified means confirmed against real hardware — one device, the Air4 Pro. Everything in the "should work" column shares a protocol with a verified device and is expected to work, but nothing else has been tested, and that includes every Galaxy Buds model: the Samsung side of this app has never been run against a real earbud. It is implemented from a reverse-engineered protocol reference and covered by unit tests over synthetic bytes, nothing more. If you try one, please open an issue either way.

Not supported: the original Galaxy Buds (2019) use different framing, and Buds+ and Buds Live use an older ambient-sound model rather than the three-way noise control this app is built around.

Features

  • The three modes, one click: Off, Noise Cancellation and Transparency as a segmented picker in the menu bar.
  • Global hotkey: ⌥ + ⌘ + N cycles modes from anywhere, without taking your hands off the keyboard or touching the buds.
  • Control Center tile: add "Earbuds Noise Mode" to Control Center and tap to cycle.
  • Shortcuts and automations: Set Noise Mode, Cycle Noise Mode, Get Noise Mode and Get Earbud Battery as App Intents. Switch to ANC when a meeting starts, or to Transparency when you unlock the Mac.
  • Per-bud battery: It indicates left and right levels in the panel.
  • Private, and quiet: One Bluetooth connection to your own earbuds. No network access, no telemetry, no account. Menu bar only: no Dock icon, no window.

Install

Manual

  1. Download BudsCtl-1.1.dmg from the latest release.
  2. Open it and drag BudsCtl into Applications.
  3. Launch BudsCtl, an ear icon appears in your menu bar.

⚠️ Not yet notarized. Releases are signed, but with a development certificate, so Gatekeeper will refuse to open the app on a Mac that isn't mine. Until that changes, either build from source (below) or clear the quarantine flag yourself:

xattr -dr com.apple.quarantine /Applications/BudsCtl.app

Only do that for software you're willing to trust, which is a good argument for building it yourself.

Getting started

  1. Pair your earbuds with the Mac in System Settings ▸ Bluetooth, as usual. BudsCtl controls buds macOS is already connected to; it doesn't replace pairing.
  2. Click the menu bar icon, open Settings at the bottom of the panel, and pick your earbuds from the list. Connected devices appear without any scan. If yours doesn't, take a bud out of the case and hit Scan for More.
  3. Grant Bluetooth access when macOS asks. Without it the app can't see any earbuds.
  4. Turn on Launch at login so the menu bar control is simply always there.

Four ways to switch

How
Menu bar Click the icon, click a mode. Shows current mode and battery.
Hotkey ⌥⌘N cycles (Off, ANC, Transparency). Rebind it in Settings.
Control Center Edit Control Center, add Earbuds Noise Mode, tap to cycle.
Shortcuts Four intents, usable in Shortcuts, Focus filters and automations.

Control Center and Shortcuts work even when the app isn't running, the request is queued and BudsCtl launches to apply it.

How it works (in plain terms)

There is no public API for this. The earbuds speak GAIA V2 Qualcomm's control protocol over Bluetooth LE, so the protocol here was reverse engineered from the traffic to a device I own: read the mode, set the mode, read each battery, read the firmware version. BudsCtl holds one BLE connection, keeps a small snapshot of what the buds reported, and hands that snapshot to Control Center and Shortcuts so they never have to open a connection of their own.

The awkward part isn't sending commands, it's trusting what comes back. The buds don't answer reliably in the first moments after connecting, and they don't announce it when they later settle into their saved mode. So BudsCtl re-reads a few times over the first minute and says "Reading mode…" rather than showing you a guess.

Galaxy Buds work differently enough to be worth a sentence. They speak a Samsung protocol over Bluetooth Classic RFCOMM rather than Bluetooth LE, so they go through IOBluetooth instead of CoreBluetooth — and unlike the SoundPEATS buds, they volunteer their full state the moment the connection opens and announce every mode change, including ones you make by touch or from your phone. So there is no "Reading mode…" guesswork for them: the protocol reverse-engineered by GalaxyBudsClient does the work, and this app just listens.


Under the hood

  • Protocol: GAIA V2 framed as <vendor 00 0A> <command:2> <payload…>, replies with the command's high bit set plus a status byte (GaiaFrame). Only the 0x03xx command group is even named in this codebase — 0x01xx/0x02xx contain device reset and power off, and this app has no reason to know them. Verified against firmware AIR4PRO-BS588R2E_20241112_v0.2.1. The status byte is advisory: this firmware answers 0x01 for merely unsupported commands, so state is always confirmed by reading, never inferred from an ack.
  • Transport: CoreBluetooth against the GAIA service 00001100-D102-11E1-…, write to the command characteristic, notifications on the response one (GaiaClient, GaiaTransport).
  • Mode settling: The reading taken as the link comes up is the least trustworthy one there is: it times out, or reports Off whatever the buds are really doing, and the buds then say nothing when they settle (measured at 33 s of silence after a case-exit connect). So DeviceController re-reads at 2, 5, 10, 20 and 45 s after connecting and stops the instant you set a mode yourself. Five reads per connection bounded, not polling.
  • Optimistic UI, honest UI: A set shows immediately as pendingMode during the device's ~1.4 s apply window, but mode only ever holds what the device confirmed (DeviceState). While the mode is unknown the panel spins instead of presenting a guess as a selection. The Control Center button repaints on the optimistic value too, so a tap is not silent for 1.4 s; the snapshot carries a pending flag so SetModeIntent still only reports success on a confirmed mode.
  • One connection, three front ends: The app is the only process that touches CoreBluetooth. The Control Center extension and the Shortcuts intents talk to it through StateBridge: a snapshot the agent writes and they read, a request they write and it drains, both in App Group defaults, nudged by a payload-free Darwin notification. Requests carry a monotonic seq in the same stored value as the payload — two keys meant a second process could read a fresh payload beside a stale seq and apply one Control Center tap twice.
  • Cold start: If nothing picks a request up within 2 s the agent isn't running, so the intent calls continueInForeground() to launch it. The agent drains pending requests on start, so the original request still lands and is never re-posted (Intents).
  • Only one Control Center control: macOS 26.5 has no ControlWidgetBundle, and ControlWidgetConfigurationBuilder takes a single configuration, so per-mode direct controls can't be built in one extension. Hence one cycle button (Controls.swift).

Build from source

brew install xcodegen
xcodegen generate
xcodebuild -project BudsCtl.xcodeproj -scheme BudsCtl -configuration Debug \
  -derivedDataPath .build/xcode build
cp -R .build/xcode/Build/Products/Debug/BudsCtl.app /Applications/
open /Applications/BudsCtl.app

project.yml is the source of truth; BudsCtl.xcodeproj, App/Info.plist and Controls/Info.plist are build artifacts and are not committed.

Building under your own team

The build is tied to an Apple Team ID JCXZ7458UT here and a fork has to change it. Not for signing's sake, the menu bar agent and the Control Center extension are two sandboxed processes, and the only way they can share state is an App Group, whose ID macOS requires to be <TeamID>.<something> and checks against the signature at launch. If you add wrong prefix, the two processes quietly read different stores. App says "Shared storage unavailable" in Settings and Control Center shows nothing while the menu bar keeps working.

Two lines to change:

Where What
project.yml DEVELOPMENT_TEAM: your 10-character Team ID
Identifiers.swift teamID the same value, for the Swift side

The entitlements files name no team at all: they derive the group from $(DEVELOPMENT_TEAM), expanded at build time. (Deliberately not the more familiar $(TeamIdentifierPrefix) that one is populated from a provisioning profile, and this app embeds none, so it would expand to empty and break the App Group silently.) A free Apple ID is enough to build and run it yourself; see Install for what it is not enough for.

Test

swift test                       # 132 tests, no hardware needed
swift run budsctl-cli status     # against real hardware
swift run budsctl-cli samsung <mac>  # verify the Galaxy Buds SPP protocol against real hardware

The BLE layer was proven with the CLI before any UI existed, and it's still the fastest way to see what the buds are doing:

budsctl-cli discover           scan for earbuds and save the chosen one
budsctl-cli status             connect and print mode, battery, firmware
budsctl-cli set <mode>         normal | anc | passthrough
budsctl-cli watch              print every frame the device sends, until Ctrl-C

Packaging

./Tools/makedmg.sh             # Release build → BudsCtl-<version>.dmg

hdiutil ships with macOS, so there's nothing to install. The script signs nothing of its own — the app carries whatever identity built it, which is why current releases are development-signed rather than notarized.

Limitations

  • One device, verified: Only SoundPEATS Air4 Pro on firmware v0.2.1 has been tested against real hardware. Other GAIA devices may work; expect nothing. The Galaxy Buds side has not been run against any real earbud at all — see Supported earbuds.
  • Not notarized: Gatekeeper blocks the released DMG. See the install note.
  • macOS 26+ and Apple Silicon: The project targets macOS 26 with Swift 6 strict concurrency. There's no back-deployment.
  • One Control Center control: For the SDK reason above cycle, not three buttons.
  • Reverse engineered, not documented: GAIA V2 is Qualcomm's, undocumented publicly, and derived here from traffic to my own device. A firmware update could change it. The Samsung SPP side is reverse engineered too, but from a third-party protocol reference rather than a packet capture of a device I own — see Supported earbuds.

Credits & license

Global hotkey handling uses KeyboardShortcuts by Sindre Sorhus (MIT).

The Samsung SPP protocol implementation was written against the reference kept by GalaxyBudsClient.

BudsCtl is not affiliated with, endorsed by, or supported by SoundPEATS, Qualcomm, or Samsung. GAIA is Qualcomm's protocol and SPP is Samsung's; the implementations here were written for interoperability, the GAIA one with a device the author owns and the Samsung one from a reverse-engineered protocol reference only.

About

Noise-cancellation control for SoundPEATS earbuds on macOS

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages