|
|
@@ -0,0 +1,487 @@ |
|
|
// |
|
|
// 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; |
|
|
} |
|
|
|
|
|
|