Skip to content

Instantly share code, notes, and snippets.

@kyleneideck
Created November 19, 2017 12:10
Show Gist options
  • Save kyleneideck/67db7999a29046a26a3608edfe82c824 to your computer and use it in GitHub Desktop.
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.
//
// headphones-detect.c
// Kyle Neideck, kyle@bearisdriving.com
//
// 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