Skip to content

Instantly share code, notes, and snippets.

@depthlove
Forked from kyleneideck/headphones-detect.c
Created October 11, 2019 12:54
Show Gist options
  • Save depthlove/896ab00194e25e19bc0b306d989acda4 to your computer and use it in GitHub Desktop.
Save depthlove/896ab00194e25e19bc0b306d989acda4 to your computer and use it in GitHub Desktop.

Revisions

  1. @kyleneideck kyleneideck created this gist Nov 19, 2017.
    487 changes: 487 additions & 0 deletions headphones-detect.c
    Original file line number Diff line number Diff line change
    @@ -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;
    }