On iOS, AirPods get a noise-control toggle right in Control Center, so you can switch between noise cancellation, transparency, and off without opening an app. Plenty of non-Apple headphones do all three too. Mine do. They just don’t get the toggle, because iOS only offers it for hardware it recognizes.
A few years ago I wanted it for my Sony WH-1000XM3s. It turned into a jailbreak tweak that I wrote twice. The first version worked without ever talking to the headphones; the second one had to.
This is an old-fashioned jailbreak tweak and an unofficial personal project. Sony, Bose, Anker, and AirPods are trademarks of their respective owners, and nothing here is affiliated with or endorsed by any of them.
Convincing iOS to show the toggle
The whole thing hangs on one object: AVOutputDevice, which iOS uses to
represent the connected audio device. The Control Center noise control asks it
which listening modes are available, and tells it which one to switch to. Hook
those two methods and you own the toggle. (%hook overrides an Objective-C
method and %orig calls the original, both Logos directives from the jailbreak
tooling.)
%hook AVOutputDevice
-(id)availableBluetoothListeningModes {
if ([preferences boolForKey:@"Enabled"] && [self.name isEqual:[preferences objectForKey:@"HeadphonesName"]]) {
return @[@"AVOutputDeviceBluetoothListeningModeNormal",
@"AVOutputDeviceBluetoothListeningModeActiveNoiseCancellation",
@"AVOutputDeviceBluetoothListeningModeAudioTransparency"];
}
return %orig;
}
When the connected device’s name matches the one you configured, iOS is handed
the same three modes AirPods Pro report, so it draws the toggle. Tapping a mode
calls setCurrentBluetoothListeningMode:error:. The two versions of the tweak
differ only in what that method does next.
Version one: borrow the manufacturer’s app
Sending the actual command means speaking the headphones’ own Bluetooth
protocol, which I hadn’t reverse-engineered yet. Sony ships an app that already
knows how (bundle id jp.co.sony.songpal.mdr). So the first version doesn’t
decode anything. It uses that app as an engine.
When you tap a mode, the tweak launches Sony’s app suspended, so it never appears on screen, and fires a distributed notification with the mode you picked:
-(BOOL)setCurrentBluetoothListeningMode:(id)arg1 error:(id*)arg2 {
shouldChangeToMode = arg1;
if (isEnabled && [self.name isEqual:headphonesName]) {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] launchApplicationWithIdentifier:@"jp.co.sony.songpal.mdr" suspended:1];
});
[[objc_getClass("NSDistributedNotificationCenter") defaultCenter]
postNotificationName:@"com.semvis123.sonyfy/setNC"
object:nil userInfo:@{@"mode": arg1} deliverImmediately:YES];
return true;
}
return %orig;
}
A second tweak, injected into the Sony app itself, listens for that
com.semvis123.sonyfy/setNC notification and drives the app’s own Bluetooth code
to send the command. The app is a puppet; the two halves talk across the process
boundary with distributed notifications. There is a small handshake for timing:
the injected side posts appLaunched once it comes up, and the main side replies
with the pending mode, so a toggle that arrives before the app is ready still
lands.
The app sometimes hangs, and the fix is another notification that force-terminates it with a private BackBoardServices call and starts it again:
BKSTerminateApplicationForReasonAndReportWithDescription(@"jp.co.sony.songpal.mdr", 1, 0, 0);
[[UIApplication sharedApplication] launchApplicationWithIdentifier:@"jp.co.sony.songpal.mdr" suspended:1];
It worked. It also meant keeping Sony’s app alive in the background just to change a setting, which is exactly the battery cost you would expect from running a whole app to send a few bytes. I shipped it anyway, and called it Sonyfy.
Version two: speak the protocol
Sonitus throws the app away and talks to the headphones directly. Working out how meant logging what the manufacturer apps actually sent, which I did with ealogger, a small External Accessory traffic logger I wrote for the job. The logs showed the headphones don’t all speak Bluetooth to iOS the same way.
Sony and Bose show up as MFi accessories, through Apple’s Made-for-iPhone
External Accessory framework. iOS hands you an EAAccessory with a list of
protocol strings, and you open a session and exchange bytes over a named tunnel:
for (EAAccessory *accessory in [[EAAccessoryManager sharedAccessoryManager] connectedAccessories]) {
if ([accessory.protocolStrings containsObject:@"jp.co.sony.songpal.mdr.link"]) {
[[SonyController sharedInstance] setCurrentBluetoothListeningMode:arg1 forAccessory:accessory ...];
} else if ([accessory.protocolStrings containsObject:@"com.bose.bmap"]) {
[[BoseController sharedInstance] ...];
}
}
Over that session, the command is a small framed packet: a start byte 0x3e, the
payload, a one-byte checksum that is just the sum of the payload bytes, and an end
byte 0x3c.
unsigned char sum = 0;
for (int i = 0; i < commandSize; i++) sum += command[i];
char commandPacked[1 + commandSize + 2];
commandPacked[0] = 0x3e; // start
memcpy(&commandPacked[1], command, commandSize);
commandPacked[1 + commandSize] = sum; // checksum
commandPacked[1 + commandSize + 1] = 0x3c; // end
There is also a single ping-pong bit in the header, a one-bit sequence number that flips between messages once a session is open. It barely matters in practice: most of these headphones stop checking it after they connect, which is why a couple of captured packets are enough to rebuild a working command. Two of those captures look like this:
0x3e 0x0c 0x00 0x00 0x00 0x00 0x07 0x68 0x17 0x01 0x01 0x01 0x00 0x14 0xffffffa9 0x3c // transparency
0x3e 0x0c 0x00 0x00 0x00 0x00 0x07 0x68 0x17 0x01 0x01 0x00 0x00 0x14 0xffffffa8 0x3c // noise canceling
The 0xffffffa9 is the checksum byte 0xa9, sign-extended because it printed as
a signed char. Strip the start byte off the front and the checksum and 0x3c
off the end, and everything in between is the payload the sum runs over.
The half that isn’t MFi
Anker’s Soundcore earbuds are the exception. They are not an MFi accessory at all.
There is no EAAccessory, no protocol string, nothing in the External Accessory
list. They are a plain Bluetooth Low Energy peripheral, so reaching them means
dropping down to CoreBluetooth and doing the whole dance yourself: scan for the
service, connect, discover services, discover characteristics, and write to the
right one. I found the service and characteristic UUIDs the same way, with the
bleLogger branch of the
same tool.
[self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:@"DAF51C01"],
[CBUUID UUIDWithString:@"DAF51A01"]] ...];
// on discovery: connect, discoverServices, discoverCharacteristics, then:
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"7777"]]) {
self.foundCharacteristic = characteristic; // write the mode command here
}
So the same toggle drives two different Bluetooth stacks depending on the brand: an MFi External Accessory session for Sony and Bose, raw GATT (the BLE service-and-characteristic dance above) for Soundcore. The tweak picks at runtime, by whichever one the headphones answer to.
BLE has a constraint MFi handled for you: a GATT peripheral serves one central at a time. Anker’s own app, if it is open, is already that central and holds the connection. So before Sonitus can connect, it has to get the app out of the way:
const char *args[] = {"killall", "-9", "SoundCore", NULL};
posix_spawn(&pid, "/usr/bin/killall", NULL, NULL, (char *const *)args, NULL);
To change a setting on my earbuds, my code sends SIGKILL to the manufacturer’s
app. Not elegant, but GATT allows one owner of the connection at a time, and it
needs to be me.
Two versions, one toggle
The first version never learned the protocol and leaned on an app that had. The second learned it, and found there was more than one: a single toggle sitting on two unrelated iOS Bluetooth APIs, MFi External Accessory for Sony and Bose and raw BLE for Anker, split by how each vendor chose to talk to iPhones rather than by anything about noise cancellation. The UI is identical for every headphone. Everything under it is per-device.
Both versions are on GitHub: the app-puppeteering Sonyfy and its direct-to-Bluetooth successor Sonitus.