Created
November 19, 2017 12:10
-
-
Save kyleneideck/67db7999a29046a26a3608edfe82c824 to your computer and use it in GitHub Desktop.
Runs a command when headphones are plugged in to or unplugged from the built-in audio device.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// headphones-detect.c | |
// Kyle Neideck, [email protected] | |
// | |
// Compile with: | |
// clang -framework CoreAudio -framework CoreFoundation -o headphones-detect headphones-detect.c | |
// | |
// Runs a command when headphones are plugged in to or unplugged from the | |
// built-in audio device. | |
// | |
// Uses code from https://stackoverflow.com/a/14490863/1091063 and | |
// https://stackoverflow.com/a/4577271/1091063. | |
// | |
#include <CoreAudio/CoreAudio.h> | |
#include <CoreFoundation/CoreFoundation.h> | |
#include <stdio.h> | |
#define DEBUG 0 | |
// Function prototypes | |
OSStatus listener_proc(AudioObjectID inObjectID, | |
UInt32 inNumberAddresses, | |
const AudioObjectPropertyAddress* inAddresses, | |
void* inClientData); | |
void handle_datasource_notification(AudioObjectID inDeviceID); | |
void print_device_name(const char* inPrefix, AudioObjectID inDeviceID); | |
int has_output_streams(AudioObjectID inDeviceID); | |
OSStatus get_output_device_list(AudioObjectID** outDeviceIDs, | |
UInt32* outDeviceCount); | |
AudioObjectID built_in_device_id(AudioObjectID* inDeviceIDs, | |
UInt32 inDeviceCount); | |
OSStatus add_datasource_listener(AudioObjectID inBuiltInDeviceID); | |
OSStatus add_device_list_listener(AudioObjectID inBuiltInDeviceID); | |
OSStatus scan_devices(AudioObjectID inPrevBuiltInDeviceID, | |
AudioObjectID* outBuiltInDeviceID); | |
OSStatus parse_args(int argc, char** argv); | |
// Globals | |
static const AudioObjectPropertyAddress kDataSourceAddr = { | |
.mSelector = kAudioDevicePropertyDataSource, | |
.mScope = kAudioObjectPropertyScopeOutput, | |
.mElement = kAudioObjectPropertyElementMaster | |
}; | |
typedef struct HeadphonesCommands { | |
char* pluggedIn; | |
char* unplugged; | |
} HeadphonesCommands; | |
static HeadphonesCommands gHeadphonesCommands = { | |
.pluggedIn = NULL, | |
.unplugged = NULL | |
}; | |
// CoreAudio will call this function when headphones are plugged in or | |
// unplugged. | |
OSStatus listener_proc(AudioObjectID inObjectID, | |
UInt32 inNumberAddresses, | |
const AudioObjectPropertyAddress* inAddresses, | |
void* inClientData) { | |
// Loop through the notifications and handle them. | |
for (UInt32 i = 0; i < inNumberAddresses; i++) { | |
switch (inAddresses[i].mSelector) { | |
case kAudioDevicePropertyDataSource: | |
handle_datasource_notification(inObjectID); | |
break; | |
case kAudioHardwarePropertyDevices: | |
// Rescan the device list because the AudioObjectID of the | |
// built-in device may have changed and our listener may have | |
// been removed from it. | |
scan_devices((AudioObjectID)inClientData, NULL); | |
break; | |
default: | |
// Ignore notifications we haven't subscribed for. | |
break; | |
} | |
} | |
// Should always return 0. See AudioObjectPropertyListenerProc in | |
// AudioHardware.h. | |
return 0; | |
} | |
void handle_datasource_notification(AudioObjectID inDeviceID) { | |
// Get the ID of the current datasource of the built-in audio device. | |
UInt32 dataSourceId = 0; | |
UInt32 dataSourceIdSize = sizeof(UInt32); | |
OSStatus err = AudioObjectGetPropertyData(inDeviceID, | |
&kDataSourceAddr, | |
0, | |
NULL, | |
&dataSourceIdSize, | |
&dataSourceId); | |
if (err != kAudioHardwareNoError) { | |
fprintf(stderr, | |
"Error getting the datasource of the built-in audio" | |
" device. (%i)\n", | |
err); | |
return; | |
} | |
char* command = NULL; | |
if (dataSourceId == 'hdpn') { | |
// Recognized as "Headphones". | |
printf("Headphones plugged in.\n"); | |
command = gHeadphonesCommands.pluggedIn; | |
} else if (dataSourceId == 'ispk') { | |
// Recognized as "Internal Speakers". | |
printf("Headphones unplugged.\n"); | |
command = gHeadphonesCommands.unplugged; | |
} | |
// Run the command. | |
if (command) { | |
#if DEBUG | |
printf("Running command:\n%s\n", command); | |
#endif | |
FILE* cmdPipe = popen(command, "r"); | |
if (!cmdPipe) { | |
#if DEBUG | |
fprintf(stderr, "!cmdPipe"); | |
#endif | |
return; | |
} | |
// Close the pipe immediately. Note that this blocks until the command | |
// process exits. | |
int status = pclose(cmdPipe); | |
#if DEBUG | |
if (status == -1) { | |
perror("pclose failed"); | |
} else { | |
printf("Command returned status: %i\n", status); | |
} | |
#endif | |
} | |
} | |
void print_device_name(const char* inPrefix, AudioObjectID inDeviceID) { | |
AudioObjectPropertyAddress deviceNameAddr = { | |
.mSelector = kAudioObjectPropertyName, | |
.mScope = kAudioObjectPropertyScopeGlobal, | |
.mElement = kAudioObjectPropertyElementMaster | |
}; | |
if (AudioObjectHasProperty(inDeviceID, &deviceNameAddr)) { | |
CFStringRef deviceName = NULL; | |
UInt32 deviceNameSize = sizeof(CFStringRef); | |
OSStatus err = AudioObjectGetPropertyData(inDeviceID, | |
&deviceNameAddr, | |
0, | |
NULL, | |
&deviceNameSize, | |
&deviceName); | |
if (err == kAudioHardwareNoError && deviceName) { | |
printf("%s%s\n", | |
inPrefix, | |
CFStringGetCStringPtr(deviceName, kCFStringEncodingUTF8)); | |
CFRelease(deviceName); | |
} | |
} | |
} | |
int has_output_streams(AudioObjectID inDeviceID) { | |
AudioObjectPropertyAddress outputStreamsAddr = { | |
.mSelector = kAudioDevicePropertyStreams, | |
.mScope = kAudioObjectPropertyScopeOutput, | |
.mElement = kAudioObjectPropertyElementMaster | |
}; | |
UInt32 outputStreamsSize = 0; | |
OSStatus err = AudioObjectGetPropertyDataSize(inDeviceID, | |
&outputStreamsAddr, | |
0, | |
NULL, | |
&outputStreamsSize); | |
return err == kAudioHardwareNoError && outputStreamsSize > 0; | |
} | |
OSStatus get_output_device_list(AudioObjectID** outDeviceIDs, | |
UInt32* outDeviceCount) { | |
AudioObjectPropertyAddress deviceListAddr = { | |
.mSelector = kAudioHardwarePropertyDevices, | |
.mScope = kAudioObjectPropertyScopeGlobal, | |
.mElement = kAudioObjectPropertyElementMaster | |
}; | |
UInt32 deviceListSize = 0; | |
OSStatus err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, | |
&deviceListAddr, | |
0, | |
NULL, | |
&deviceListSize); | |
if (err != kAudioHardwareNoError) { | |
fprintf(stderr, | |
"AudioObjectGetPropertyDataSize (kAudioHardwarePropertyDevices)" | |
" failed: %i\n", | |
err); | |
return err; | |
} | |
UInt32 deviceCount = deviceListSize / sizeof(AudioDeviceID); | |
AudioObjectID* deviceIDs = (AudioObjectID*)malloc(deviceListSize); | |
if (!deviceIDs) { | |
fprintf(stderr, "!deviceIDs\n"); | |
return kAudioHardwareIllegalOperationError; | |
} | |
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, | |
&deviceListAddr, | |
0, | |
NULL, | |
&deviceListSize, | |
deviceIDs); | |
if (err == kAudioHardwareNoError) { | |
// Return the device list. | |
if (outDeviceCount) { | |
*outDeviceCount = deviceCount; | |
} | |
if (outDeviceIDs) { | |
*outDeviceIDs = deviceIDs; | |
} | |
} else { | |
fprintf(stderr, | |
"AudioObjectGetPropertyData (kAudioHardwarePropertyDevices)" | |
" failed: %i\n", | |
err); | |
} | |
return err; | |
} | |
AudioObjectID built_in_device_id(AudioObjectID* inDeviceIDs, UInt32 inDeviceCount) { | |
for (UInt32 i = 0; i < inDeviceCount; i++) { | |
AudioObjectID deviceID = inDeviceIDs[i]; | |
#if DEBUG | |
print_device_name("Device: ", deviceID); | |
#endif | |
AudioObjectPropertyAddress transportTypeAddr = { | |
.mSelector = kAudioDevicePropertyTransportType, | |
.mScope = kAudioObjectPropertyScopeGlobal, | |
.mElement = kAudioObjectPropertyElementMaster | |
}; | |
if (AudioObjectHasProperty(deviceID, &transportTypeAddr)) { | |
#if DEBUG | |
printf("...has transport type.\n"); | |
#endif | |
UInt32 transportType = kAudioDeviceTransportTypeUnknown; | |
UInt32 transportTypeSize = sizeof(UInt32); | |
OSStatus err = AudioObjectGetPropertyData(deviceID, | |
&transportTypeAddr, | |
0, | |
NULL, | |
&transportTypeSize, | |
&transportType); | |
if (err == kAudioHardwareNoError && | |
transportType == kAudioDeviceTransportTypeBuiltIn && | |
has_output_streams(deviceID)) { | |
// Found it. | |
#if DEBUG | |
printf("...is built-in device.\n"); | |
#endif | |
return deviceID; | |
} | |
} | |
} | |
// Didn't find it. | |
return kAudioObjectUnknown; | |
} | |
OSStatus add_datasource_listener(AudioObjectID inBuiltInDeviceID) { | |
if (!AudioObjectHasProperty(inBuiltInDeviceID, &kDataSourceAddr)) { | |
fprintf(stderr, | |
"Error: No datasources found for the built-in audio" | |
" device.\n"); | |
return kAudioHardwareUnsupportedOperationError; | |
} | |
OSStatus err = AudioObjectAddPropertyListener(inBuiltInDeviceID, | |
&kDataSourceAddr, | |
listener_proc, | |
NULL); | |
return err; | |
} | |
OSStatus add_device_list_listener(AudioObjectID inBuiltInDeviceID) { | |
AudioObjectPropertyAddress deviceListAddr = { | |
.mSelector = kAudioHardwarePropertyDevices, | |
.mScope = kAudioObjectPropertyScopeGlobal, | |
.mElement = kAudioObjectPropertyElementMaster | |
}; | |
OSStatus err = | |
AudioObjectAddPropertyListener(kAudioObjectSystemObject, | |
&deviceListAddr, | |
listener_proc, | |
(void*)(intptr_t)inBuiltInDeviceID); | |
if (err != kAudioHardwareNoError) { | |
fprintf(stderr, | |
"AudioObjectAddPropertyListener" | |
" (kAudioHardwarePropertyDevices) failed: %i\n", | |
err); | |
} | |
return err; | |
} | |
OSStatus scan_devices(AudioObjectID inPrevBuiltInDeviceID, | |
AudioObjectID* outBuiltInDeviceID) { | |
// Get a list of the connected audio output devices. | |
AudioObjectID* deviceIDs; | |
UInt32 deviceCount; | |
OSStatus err = get_output_device_list(&deviceIDs, &deviceCount); | |
if (err != kAudioHardwareNoError) { | |
return err; | |
} | |
// Get the ID of the built-in audio device. | |
AudioObjectID builtInDeviceID = built_in_device_id(deviceIDs, deviceCount); | |
free(deviceIDs); | |
deviceIDs = NULL; | |
if (builtInDeviceID == kAudioObjectUnknown) { | |
fprintf(stderr, "Couldn't find the built-in audio device.\n"); | |
return kAudioHardwareIllegalOperationError; | |
} | |
int idChanged = (builtInDeviceID != inPrevBuiltInDeviceID); | |
if (idChanged) { | |
// Try to remove our listener from the previous device. This will | |
// probably fail because that device probably doesn't exist anymore. | |
AudioObjectRemovePropertyListener(inPrevBuiltInDeviceID, | |
&kDataSourceAddr, | |
listener_proc, | |
NULL); | |
} | |
// Listen for datasource changes, which tell us when the headphones have | |
// been plugged in or unplugged. | |
// | |
// For other devices it might be better to listen to | |
// kAudioDevicePropertyJackIsConnected, but the built-in device doesn't | |
// support it. | |
err = add_datasource_listener(builtInDeviceID); | |
if (err == kAudioHardwareNoError) { | |
if (idChanged) { | |
print_device_name("Listening for headphones being plugged in to or" | |
" unplugged from device: ", | |
builtInDeviceID); | |
} | |
// Return the device ID. | |
if (outBuiltInDeviceID) { | |
*outBuiltInDeviceID = builtInDeviceID; | |
} | |
} else { | |
#if DEBUG | |
// Only log this when debugging because it might have failed because | |
// the listener was already registered, which is fine. The CoreAudio | |
// API doesn't have a way to check whether a listener is registered or | |
// to find out when it gets unregistered, e.g. because coreaudiod was | |
// restarted. | |
fprintf(stderr, "add_datasource_listener failed: %i\n", err); | |
#endif | |
} | |
return err; | |
} | |
OSStatus parse_args(int argc, char** argv) { | |
if (argc < 2) { | |
char* executableName = (argc == 0 ? "headphones-detect" : argv[0]); | |
fprintf(stderr, | |
"Usage: %s plugged-in-command [unplugged-command]\n", | |
executableName); | |
fprintf(stderr, "where\n"); | |
fprintf(stderr, | |
" - plugged-in-command is the command to run when headphones" | |
" are plugged in, and\n"); | |
fprintf(stderr, | |
" - unplugged-command is the optional command to run when" | |
" headphones are unplugged.\n\n"); | |
fprintf(stderr, | |
"If unplugged-command is omitted, plugged-in-command will be" | |
" run in both cases.\n\n"); | |
fprintf(stderr, | |
"%s calls a command by passing it as the argument to" | |
" \"/bin/sh -c\" and waits for the command to finish before" | |
" continuing. (In general, you should be able to add \" &\" to" | |
" the end of long-running commands to have %s continue" | |
" immediately.)\n\n", | |
executableName, | |
executableName); | |
fprintf(stderr, | |
"The following example will open iTunes when the headphones are" | |
" plugged in and close it when they're unplugged.\n" | |
"./headphones-detect 'open /Applications/iTunes.app' 'osascript" | |
" -e \"tell application \\\"iTunes\\\" to quit\"'\n"); | |
return kAudioHardwareUnspecifiedError; | |
} | |
gHeadphonesCommands.pluggedIn = argv[1]; | |
gHeadphonesCommands.unplugged = (argc < 3 ? argv[1] : argv[2]); | |
printf("Headphones plugged in command:\n%s\n", | |
gHeadphonesCommands.pluggedIn); | |
printf("Headphones unplugged command:\n%s\n", | |
gHeadphonesCommands.unplugged); | |
return kAudioHardwareNoError; | |
} | |
int main(int argc, char** argv) { | |
// Read the commands to run headphones are plugged in or unplugged. | |
OSStatus err = parse_args(argc, argv); | |
if (err != kAudioHardwareNoError) { | |
return EXIT_FAILURE; | |
} | |
// Find the built-in audio device and register for notifications when | |
// headphones are plugged in or unplugged. | |
AudioObjectID builtInDeviceID; | |
err = scan_devices(kAudioObjectUnknown, &builtInDeviceID); | |
if (err != kAudioHardwareNoError) { | |
return EXIT_FAILURE; | |
} | |
// Register for notifications when the list of audio devices changes so we | |
// can rescan the list. This handles things like coreaudiod (the CoreAudio | |
// daemon process) restarting. | |
err = add_device_list_listener(builtInDeviceID); | |
if (err != kAudioHardwareNoError) { | |
return EXIT_FAILURE; | |
} | |
printf("Press Ctrl+C to quit.\n"); | |
// Start the main loop. | |
CFRunLoopRun(); | |
return EXIT_SUCCESS; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment