/*****************************************************************************
 * auhal.c: AUHAL and Coreaudio output plugin
 *****************************************************************************
 * Copyright (C) 2005 - 2017 VLC authors and VideoLAN
 *
 * Authors: Derk-Jan Hartman <hartman at videolan dot org>
 *          Felix Paul Kühne <fkuehne at videolan dot org>
 *          David Fuhrmann <david dot fuhrmann at googlemail dot com>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

#pragma mark includes

#include "coreaudio_common.h"

#include <vlc_configuration.h>
#include <vlc_plugin.h>
#include <vlc_threads.h>
#include <vlc_dialog.h>                      // vlc_dialog_display_error
#include <vlc_charset.h>                     // FromCFString

#include <CoreAudio/CoreAudio.h>             // AudioDeviceID
#include <CoreServices/CoreServices.h>

#include <IOKit/audio/IOAudioTypes.h>

#pragma mark -
#pragma mark local prototypes & module descriptor


#define AOUT_VOLUME_DEFAULT             256
#define AOUT_VOLUME_MAX                 512

#define VOLUME_TEXT N_("Audio volume")
#define DEVICE_TEXT N_("Last audio device")

static int      Open                    (vlc_object_t *);
static void     Close                   (vlc_object_t *);

vlc_module_begin ()
    set_shortname("auhal")
    set_description(N_("HAL AudioUnit output"))
    set_capability("audio output", 101)
    set_subcategory(SUBCAT_AUDIO_AOUT)
    set_callbacks(Open, Close)
    add_integer("auhal-volume", AOUT_VOLUME_DEFAULT,
                VOLUME_TEXT, NULL)
    change_integer_range(0, AOUT_VOLUME_MAX)
    add_string("auhal-audio-device", "", DEVICE_TEXT, NULL)
    add_string("auhal-warned-devices", "", NULL, NULL)
    change_private()
vlc_module_end ()

#pragma mark -
#pragma mark private declarations

#define AOUT_VAR_SPDIF_FLAG 0xf00000

/*****************************************************************************
 * aout_sys_t: private audio output method descriptor
 *****************************************************************************
 * This structure is part of the audio output thread descriptor.
 * It describes the CoreAudio specific properties of an output thread.
 *****************************************************************************/
typedef struct
{
    struct aout_sys_common c;

    /* DeviceID of the selected device */
    AudioObjectID               i_selected_dev;
    /* DeviceID of device which will be selected on start */
    AudioObjectID               i_new_selected_dev;
    /* true if the user selected the default audio device (id 0) */
    bool                        b_selected_dev_is_default;

    /* DeviceID of current device */
    AudioDeviceIOProcID         i_procID;
    /* Are we running in digital mode? */
    bool                        b_digital;

    /* AUHAL specific */
    AudioUnit                   au_unit;

    /* CoreAudio SPDIF mode specific */
    /* Keep the pid of our hog status */
    pid_t                       i_hog_pid;
    /* The StreamID that has a cac3 streamformat */
    AudioStreamID               i_stream_id;
    /* The index of i_stream_id in an AudioBufferList */
    int                         i_stream_index;
    /* The original format of the stream */
    AudioStreamBasicDescription sfmt_revert;
    /* Whether we need to revert the stream format */
    bool                        b_revert;
    /* Whether we need to set the mixing mode back */
    bool                        b_changed_mixing;

    CFArrayRef                  device_list;
    /* protects access to device_list */
    vlc_mutex_t                 device_list_lock;

    /* Synchronizes access to i_selected_dev. This is only needed between VLCs
     * audio thread and the core audio callback thread. The value is only
     * changed in Start, further access to this variable within the audio
     * thread (start, stop, close) needs no protection. */
    vlc_mutex_t                 selected_device_lock;

    float                       f_volume;

    bool                        b_ignore_streams_changed_callback;
} aout_sys_t;

#pragma mark -
#pragma mark helpers

static int
AoGetProperty(audio_output_t *p_aout, AudioObjectID id,
              const AudioObjectPropertyAddress *p_address, size_t i_elm_size,
              ssize_t i_nb_expected_elms, size_t *p_nb_elms, void **pp_out_data,
              void *p_allocated_out_data)
{
    assert(i_elm_size > 0);

    /* Get data size */
    UInt32 i_out_size;
    OSStatus err = AudioObjectGetPropertyDataSize(id, p_address, 0, NULL,
                                                  &i_out_size);
    if (err != noErr)
    {
        msg_Err(p_aout, "AudioObjectGetPropertyDataSize failed, device id: %i, "
                "prop: [%4.4s], OSStatus: %d", id, (const char *) &p_address[0],
                (int)err);
        return VLC_EGENERIC;
    }

    size_t i_nb_elms = i_out_size / i_elm_size;
    if (p_nb_elms != NULL)
        *p_nb_elms = i_nb_elms;
    /* Check if we get the expected number of elements */
    if (i_nb_expected_elms != -1 && (size_t)i_nb_expected_elms != i_nb_elms)
    {
        msg_Err(p_aout, "AoGetProperty error: expected elements don't match");
        return VLC_EGENERIC;
    }

    if (pp_out_data == NULL && p_allocated_out_data == NULL)
        return VLC_SUCCESS;

    if (i_out_size == 0)
    {
        if (pp_out_data != NULL)
            *pp_out_data = NULL;
        return VLC_SUCCESS;
    }

    /* Alloc data or use pre-allocated one */
    void *p_out_data;
    if (pp_out_data != NULL)
    {
        assert(p_allocated_out_data == NULL);

        *pp_out_data = malloc(i_out_size);
        if (*pp_out_data == NULL)
            return VLC_ENOMEM;
        p_out_data = *pp_out_data;
    }
    else
    {
        assert(p_allocated_out_data != NULL);
        p_out_data = p_allocated_out_data;
    }

    /* Fill data */
    err = AudioObjectGetPropertyData(id, p_address, 0, NULL, &i_out_size,
                                     p_out_data);
    if (err != noErr)
    {
        msg_Err(p_aout, "AudioObjectGetPropertyData failed, device id: %i, "
                "prop: [%4.4s], OSStatus: %d", id, (const char *) &p_address[0],
                (int) err);

        if (pp_out_data != NULL)
            free(*pp_out_data);
        return VLC_EGENERIC;
    }
    assert(p_nb_elms == NULL || *p_nb_elms == (i_out_size / i_elm_size));
    return VLC_SUCCESS;
}

/* Get Audio Object Property data: pp_out_data will be allocated by this MACRO
 * and need to be freed in case of success. */
#define AO_GETPROP(id, type, p_out_nb_elms, pp_out_data, a1, a2) \
    AoGetProperty(p_aout, (id), \
                  &(AudioObjectPropertyAddress) {(a1), (a2), 0}, sizeof(type), \
                  -1, (p_out_nb_elms), (void **)(pp_out_data), NULL)

/* Get 1 Audio Object Property data: pre-allocated by the caller */
#define AO_GET1PROP(id, type, p_out_data, a1, a2) \
    AoGetProperty(p_aout, (id), \
                  &(AudioObjectPropertyAddress) {(a1), (a2), 0}, sizeof(type), \
                  1, NULL, NULL, (p_out_data))

static bool
AoIsPropertySettable(audio_output_t *p_aout, AudioObjectID id,
                     const AudioObjectPropertyAddress *p_address)
{
    Boolean b_settable;
    OSStatus err = AudioObjectIsPropertySettable(id, p_address, &b_settable);
    if (err != noErr)
    {
        msg_Warn(p_aout, "AudioObjectIsPropertySettable failed, device id: %i, "
                 "prop: [%4.4s], OSStatus: %d", id, (const char *)&p_address[0],
                 (int)err);
        return false;
    }
    return b_settable;
}

#define AO_ISPROPSETTABLE(id, a1, a2) \
    AoIsPropertySettable(p_aout, (id), \
                         &(AudioObjectPropertyAddress) { (a1), (a2), 0})

#define AO_HASPROP(id, a1, a2) \
    AudioObjectHasProperty((id), &(AudioObjectPropertyAddress) { (a1), (a2), 0})

static int
AoSetProperty(audio_output_t *p_aout, AudioObjectID id,
              const AudioObjectPropertyAddress *p_address, size_t i_data,
              const void *p_data)
{
    OSStatus err =
        AudioObjectSetPropertyData(id, p_address, 0, NULL, i_data, p_data);

    if (err != noErr)
    {
        msg_Err(p_aout, "AudioObjectSetPropertyData failed, device id: %i, "
                 "prop: [%4.4s], OSStatus: %d", id, (const char *)&p_address[0],
                 (int)err);
        return VLC_EGENERIC;
    }
    return VLC_SUCCESS;
}

#define AO_SETPROP(id, i_data, p_data, a1, a2) \
    AoSetProperty(p_aout, (id), \
                  &(AudioObjectPropertyAddress) { (a1), (a2), 0}, \
                  (i_data), (p_data));

static int
AoUpdateListener(audio_output_t *p_aout, bool add, AudioObjectID id,
                 const AudioObjectPropertyAddress *p_address,
                 AudioObjectPropertyListenerProc listener, void *data)
{
    OSStatus err = add ?
        AudioObjectAddPropertyListener(id, p_address, listener, data) :
        AudioObjectRemovePropertyListener(id, p_address, listener, data);

    if (err != noErr)
    {
        msg_Err(p_aout, "AudioObject%sPropertyListener failed, device id %i, "
                "prop: [%4.4s], OSStatus: %d", add ? "Add" : "Remove", id,
                (const char *)&p_address[0], (int)err);
        return VLC_EGENERIC;
    }
    return VLC_SUCCESS;
}

#define AO_UPDATELISTENER(id, add, listener, data, a1, a2) \
    AoUpdateListener(p_aout, add, (id), \
                  &(AudioObjectPropertyAddress) { (a1), (a2), 0}, \
                  (listener), (data))

#pragma mark -
#pragma mark Stream / Hardware Listeners

static bool
IsAudioFormatDigital(AudioFormatID id)
{
    switch (id)
    {
        case 'IAC3':
        case 'iac3':
        case kAudioFormat60958AC3:
        case kAudioFormatAC3:
        case kAudioFormatEnhancedAC3:
            return true;
        default:
            return false;
    }
}

static OSStatus
StreamsChangedListener(AudioObjectID, UInt32,
                       const AudioObjectPropertyAddress [], void *);

static int
ManageAudioStreamsCallback(audio_output_t *p_aout, AudioDeviceID i_dev_id,
                           bool b_register)
{
    /* Retrieve all the output streams */
    size_t i_streams;
    AudioStreamID *p_streams;
    int ret = AO_GETPROP(i_dev_id, AudioStreamID, &i_streams, &p_streams,
                          kAudioDevicePropertyStreams,
                          kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
        return ret;

    for (size_t i = 0; i < i_streams; i++)
    {
        /* get notified when physical formats change */
        AO_UPDATELISTENER(p_streams[i], b_register, StreamsChangedListener,
                          p_aout, kAudioStreamPropertyAvailablePhysicalFormats,
                          kAudioObjectPropertyScopeGlobal);
    }

    free(p_streams);
    return VLC_SUCCESS;
}

/*
 * AudioStreamSupportsDigital: Checks if audio stream is compatible with raw
 * bitstreams
 */
static bool
AudioStreamSupportsDigital(audio_output_t *p_aout, AudioStreamID i_stream_id)
{
    bool b_return = false;

    /* Retrieve all the stream formats supported by each output stream */
    size_t i_formats;
    AudioStreamRangedDescription *p_format_list;
    int ret = AO_GETPROP(i_stream_id, AudioStreamRangedDescription, &i_formats,
                          &p_format_list,
                          kAudioStreamPropertyAvailablePhysicalFormats,
                          kAudioObjectPropertyScopeGlobal);
    if (ret != VLC_SUCCESS)
        return false;

    for (size_t i = 0; i < i_formats; i++)
    {
#ifndef NDEBUG
        msg_Dbg(p_aout, STREAM_FORMAT_MSG("supported format: ",
                p_format_list[i].mFormat));
#endif

        if (IsAudioFormatDigital(p_format_list[i].mFormat.mFormatID))
            b_return = true;
    }

    free(p_format_list);
    return b_return;
}

/*
 * AudioDeviceSupportsDigital: Checks if device supports raw bitstreams
 */
static bool
AudioDeviceSupportsDigital(audio_output_t *p_aout, AudioDeviceID i_dev_id)
{
    size_t i_streams;
    AudioStreamID * p_streams;
    int ret = AO_GETPROP(i_dev_id, AudioStreamID, &i_streams, &p_streams,
                          kAudioDevicePropertyStreams,
                          kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
        return false;

    for (size_t i = 0; i < i_streams; i++)
    {
        if (AudioStreamSupportsDigital(p_aout, p_streams[i]))
        {
            free(p_streams);
            return true;
        }
    }

    free(p_streams);
    return false;
}

static void
ReportDevice(audio_output_t *p_aout, UInt32 i_id, const char *name)
{
    char deviceid[10];
    sprintf(deviceid, "%i", i_id);

    aout_HotplugReport(p_aout, deviceid, name);
}

/*
 * AudioDeviceIsAHeadphone: Checks if device is a headphone
 */

static bool
AudioDeviceIsAHeadphone(audio_output_t *p_aout, AudioDeviceID i_dev_id)
{
    VLC_UNUSED(p_aout);
    UInt32 defaultSize = sizeof(AudioDeviceID);

    const AudioObjectPropertyAddress defaultAddr = {
        kAudioHardwarePropertyDefaultOutputDevice,
        kAudioObjectPropertyScopeGlobal,
        kAudioObjectPropertyElementMaster
    };

    AudioObjectGetPropertyData(kAudioObjectSystemObject, &defaultAddr, 0, NULL, &defaultSize, &i_dev_id);

    AudioObjectPropertyAddress property;
    property.mSelector = kAudioDevicePropertyDataSource;
    property.mScope = kAudioDevicePropertyScopeOutput;
    property.mElement = kAudioObjectPropertyElementMaster;

    UInt32 data;
    UInt32 size = sizeof(UInt32);
    AudioObjectGetPropertyData(i_dev_id, &property, 0, NULL, &size, &data);

    /*
     'hdpn' == headphone (kIOAudioOutputPortSubTypeHeadphones)
     'ispk' == internal speaker (kIOAudioOutputPortSubTypeInternalSpeaker)
     '61pd' == HDMI
     '    ' == Bluetooth accessory or AirPlay
    */

    return data == kIOAudioOutputPortSubTypeHeadphones;
}

/*
 * AudioDeviceHasOutput: Checks if the device is actually an output device
 */
static int
AudioDeviceHasOutput(audio_output_t *p_aout, AudioDeviceID i_dev_id)
{
    size_t i_streams;
    int ret = AO_GETPROP(i_dev_id, AudioStreamID, &i_streams, NULL,
                          kAudioDevicePropertyStreams,
                          kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS || i_streams == 0)
        return FALSE;

    return TRUE;
}

static void
RebuildDeviceList(audio_output_t * p_aout, UInt32 *p_id_exists)
{
    aout_sys_t *p_sys = p_aout->sys;

    msg_Dbg(p_aout, "Rebuild device list");

    ReportDevice(p_aout, 0, _("System Sound Output Device"));

    /* Get number of devices */
    size_t i_devices;
    AudioDeviceID *p_devices;
    int ret = AO_GETPROP(kAudioObjectSystemObject, AudioDeviceID, &i_devices,
                         &p_devices, kAudioHardwarePropertyDevices,
                         kAudioObjectPropertyScopeGlobal);

    if (ret != VLC_SUCCESS || i_devices == 0)
    {
        msg_Err(p_aout, "No audio output devices found.");
        return;
    }
    msg_Dbg(p_aout, "found %zu audio device(s)", i_devices);

    /* setup local array */
    CFMutableArrayRef currentListOfDevices =
        CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

    UInt32 i_id_exists;
    if (p_id_exists)
    {
        i_id_exists = *p_id_exists;
        *p_id_exists = 0;
    }
    else
        i_id_exists = 0;

    for (size_t i = 0; i < i_devices; i++)
    {
        CFStringRef device_name_ref;
        char *psz_name;
        UInt32 i_id = p_devices[i];

        int ret = AO_GET1PROP(i_id, CFStringRef, &device_name_ref,
                              kAudioObjectPropertyName,
                              kAudioObjectPropertyScopeGlobal);
        if (ret != VLC_SUCCESS)
        {
            msg_Dbg(p_aout, "failed to get name for device %i", i_id);
            continue;
        }

        psz_name = FromCFString(device_name_ref, kCFStringEncodingUTF8);
        CFRelease(device_name_ref);

        msg_Dbg(p_aout, "DevID: %i DevName: %s", i_id, psz_name);

        if (!AudioDeviceHasOutput(p_aout, i_id))
        {
            msg_Dbg(p_aout, "this '%s' is INPUT only. skipping...", psz_name);
            free(psz_name);
            continue;
        }

        // Report back audio device in analog mode
        if (p_id_exists && i_id == i_id_exists)
            *p_id_exists = i_id;

        ReportDevice(p_aout, i_id, psz_name);
        CFNumberRef deviceNumber = CFNumberCreate(kCFAllocatorDefault,
                                                  kCFNumberSInt32Type, &i_id);
        CFArrayAppendValue(currentListOfDevices, deviceNumber);
        CFRelease(deviceNumber);

        // TODO: only register once for each device
        ManageAudioStreamsCallback(p_aout, i_id, true);

        if (AudioDeviceSupportsDigital(p_aout, i_id))
        {
            msg_Dbg(p_aout, "'%s' supports digital output", psz_name);
            char *psz_encoded_name;
            if( asprintf(&psz_encoded_name, _("%s (Encoded Output)"), psz_name) == -1 )
            {
                free(psz_name);
                continue;
            }

            i_id = i_id | AOUT_VAR_SPDIF_FLAG;
            ReportDevice(p_aout, i_id, psz_encoded_name);
            deviceNumber = CFNumberCreate(kCFAllocatorDefault,
                                          kCFNumberSInt32Type, &i_id);
            CFArrayAppendValue(currentListOfDevices, deviceNumber);
            CFRelease(deviceNumber);
            free(psz_encoded_name);

            // Report back audio device in digital mode
            if (p_id_exists && i_id == i_id_exists)
                *p_id_exists = i_id;
        }
        free(psz_name);
    }

    vlc_mutex_lock(&p_sys->device_list_lock);
    CFIndex count = 0;
    if (p_sys->device_list)
        count = CFArrayGetCount(p_sys->device_list);
    CFRange newListSearchRange =
        CFRangeMake(0, CFArrayGetCount(currentListOfDevices));

    if (count > 0)
    {
        msg_Dbg(p_aout, "Looking for removed devices");
        CFNumberRef cfn_device_id;
        int i_device_id = 0;
        for (CFIndex x = 0; x < count; x++)
        {
            if (!CFArrayContainsValue(currentListOfDevices, newListSearchRange,
                                      CFArrayGetValueAtIndex(p_sys->device_list,
                                      x)))
            {
                cfn_device_id = CFArrayGetValueAtIndex(p_sys->device_list, x);
                if (cfn_device_id)
                {
                    CFNumberGetValue(cfn_device_id, kCFNumberSInt32Type,
                                     &i_device_id);
                    msg_Dbg(p_aout, "Device ID %i is not found in new array, "
                            "deleting.", i_device_id);

                    ReportDevice(p_aout, i_device_id, NULL);
                }
            }
        }
    }
    if (p_sys->device_list)
        CFRelease(p_sys->device_list);
    p_sys->device_list = CFArrayCreateCopy(kCFAllocatorDefault,
                                           currentListOfDevices);
    CFRelease(currentListOfDevices);
    vlc_mutex_unlock(&p_sys->device_list_lock);

    free(p_devices);
}

/*
 * Callback when current device is not alive anymore
 */
static OSStatus
DeviceAliveListener(AudioObjectID inObjectID,  UInt32 inNumberAddresses,
                    const AudioObjectPropertyAddress inAddresses[],
                    void *inClientData)
{
    VLC_UNUSED(inObjectID);
    VLC_UNUSED(inNumberAddresses);
    VLC_UNUSED(inAddresses);

    audio_output_t *p_aout = (audio_output_t *)inClientData;
    if (!p_aout)
        return -1;

    msg_Warn(p_aout, "audio device died, resetting aout");
    aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);

    return noErr;
}

/*
 * Callback when default audio device changed
 */
static OSStatus
DefaultDeviceChangedListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
                             const AudioObjectPropertyAddress inAddresses[],
                             void *inClientData)
{
    VLC_UNUSED(inObjectID);
    VLC_UNUSED(inNumberAddresses);
    VLC_UNUSED(inAddresses);

    audio_output_t *p_aout = (audio_output_t *)inClientData;
    if (!p_aout)
        return -1;

    aout_sys_t *p_sys = p_aout->sys;

    if (!p_sys->b_selected_dev_is_default)
        return noErr;

    AudioObjectID defaultDeviceID;
    int ret = AO_GET1PROP(kAudioObjectSystemObject, AudioObjectID,
                          &defaultDeviceID,
                          kAudioHardwarePropertyDefaultOutputDevice,
                          kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
        return -1;

    msg_Dbg(p_aout, "default device changed to %i", defaultDeviceID);

    /* Default device is changed by the os to allow other apps to play sound
     * while in digital mode. But this should not affect ourself. */
    if (p_sys->b_digital)
    {
        msg_Dbg(p_aout, "ignore, as digital mode is active");
        return noErr;
    }

    vlc_mutex_lock(&p_sys->selected_device_lock);
    /* Also ignore events which announce the same device id */
    if (defaultDeviceID != p_sys->i_selected_dev)
    {
        msg_Dbg(p_aout, "default device actually changed, resetting aout");
        aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
    }
    vlc_mutex_unlock(&p_sys->selected_device_lock);

    return noErr;
}

/*
 * Callback when physical formats for device change
 */
static OSStatus
StreamsChangedListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
                       const AudioObjectPropertyAddress inAddresses[],
                       void *inClientData)
{
    VLC_UNUSED(inNumberAddresses);
    VLC_UNUSED(inAddresses);

    audio_output_t *p_aout = (audio_output_t *)inClientData;
    if (!p_aout)
        return -1;

    aout_sys_t *p_sys = p_aout->sys;
    if(unlikely(p_sys->b_ignore_streams_changed_callback == true))
        return 0;

    msg_Dbg(p_aout, "available physical formats for audio device changed");
    RebuildDeviceList(p_aout, NULL);

    vlc_mutex_lock(&p_sys->selected_device_lock);
    /* In this case audio has not yet started. Below code will not work and is
     * not needed here. */
    if (p_sys->i_selected_dev == 0)
    {
        vlc_mutex_unlock(&p_sys->selected_device_lock);
        return 0;
    }

    /*
     * check if changed stream id belongs to current device
     */
    size_t i_streams;
    AudioStreamID *p_streams;
    int ret = AO_GETPROP(p_sys->i_selected_dev, AudioStreamID, &i_streams,
                         &p_streams, kAudioDevicePropertyStreams,
                         kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
    {
        vlc_mutex_unlock(&p_sys->selected_device_lock);
        return ret;
    }
    vlc_mutex_unlock(&p_sys->selected_device_lock);

    for (size_t i = 0; i < i_streams; i++)
    {
        if (p_streams[i] == inObjectID)
        {
            msg_Dbg(p_aout, "Restart aout as this affects current device");
            aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
            break;
        }
    }
    free(p_streams);

    return noErr;
}

/*
 * Callback when device list changed
 */
static OSStatus
DevicesListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
                const AudioObjectPropertyAddress inAddresses[],
                void *inClientData)
{
    VLC_UNUSED(inObjectID);
    VLC_UNUSED(inNumberAddresses);
    VLC_UNUSED(inAddresses);

    audio_output_t *p_aout = (audio_output_t *)inClientData;
    if (!p_aout)
        return -1;
    aout_sys_t *p_sys = p_aout->sys;

    msg_Dbg(p_aout, "audio device configuration changed, resetting cache");
    RebuildDeviceList(p_aout, NULL);

    vlc_mutex_lock(&p_sys->selected_device_lock);
    vlc_mutex_lock(&p_sys->device_list_lock);
    CFNumberRef selectedDevice =
        CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type,
                       &p_sys->i_selected_dev);
    CFRange range = CFRangeMake(0, CFArrayGetCount(p_sys->device_list));
    if (!CFArrayContainsValue(p_sys->device_list, range, selectedDevice))
        aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
    CFRelease(selectedDevice);
    vlc_mutex_unlock(&p_sys->device_list_lock);
    vlc_mutex_unlock(&p_sys->selected_device_lock);

    return noErr;
}

/*
 * StreamListener: check whether the device's physical format change is complete
 */
static OSStatus
StreamListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
               const AudioObjectPropertyAddress inAddresses[],
               void *inClientData)
{
    OSStatus err = noErr;
    struct { vlc_mutex_t lock; vlc_cond_t cond; } * w = inClientData;

    VLC_UNUSED(inObjectID);

    for (unsigned int i = 0; i < inNumberAddresses; i++)
    {
        if (inAddresses[i].mSelector == kAudioStreamPropertyPhysicalFormat)
        {
            int canc = vlc_savecancel();
            vlc_mutex_lock(&w->lock);
            vlc_cond_signal(&w->cond);
            vlc_mutex_unlock(&w->lock);
            vlc_restorecancel(canc);
            break;
        }
    }
    return err;
}

/*
 * AudioStreamChangeFormat: switch stream format based on the provided
 * description
 */
static int
AudioStreamChangeFormat(audio_output_t *p_aout, AudioStreamID i_stream_id,
                        AudioStreamBasicDescription change_format)
{
    int retValue = false;

    struct { vlc_mutex_t lock; vlc_cond_t cond; } w;

    msg_Dbg(p_aout, STREAM_FORMAT_MSG("setting stream format: ", change_format));

    /* Condition because SetProperty is asynchronous */
    vlc_cond_init(&w.cond);
    vlc_mutex_init(&w.lock);
    vlc_mutex_lock(&w.lock);

    /* Install the callback */
    int ret = AO_UPDATELISTENER(i_stream_id, true, StreamListener, &w,
                                kAudioStreamPropertyPhysicalFormat,
                                kAudioObjectPropertyScopeGlobal);

    if (ret != VLC_SUCCESS)
    {
        retValue = false;
        goto out;
    }

    /* change the format */
    ret = AO_SETPROP(i_stream_id, sizeof(AudioStreamBasicDescription),
                     &change_format, kAudioStreamPropertyPhysicalFormat,
                     kAudioObjectPropertyScopeGlobal);
    if (ret != VLC_SUCCESS)
    {
        retValue = false;
        goto out;
    }

    /* The AudioStreamSetProperty is not only asynchronous (requiring the
     * locks) it is also not atomic in its behaviour.  Therefore we check 9
     * times before we really give up.
     */
    for (int i = 0; i < 9; i++)
    {
        /* Callback is not always invoked. So first check if format is already
         * set. */
        if (i > 0)
        {
            vlc_tick_t timeout = vlc_tick_now() + VLC_TICK_FROM_MS(500);
            if (vlc_cond_timedwait(&w.cond, &w.lock, timeout))
                msg_Dbg(p_aout, "reached timeout");
        }

        AudioStreamBasicDescription actual_format;
        int ret = AO_GET1PROP(i_stream_id, AudioStreamBasicDescription,
                              &actual_format,
                              kAudioStreamPropertyPhysicalFormat,
                              kAudioObjectPropertyScopeGlobal);

        if (ret != VLC_SUCCESS)
            continue;

        msg_Dbg(p_aout, STREAM_FORMAT_MSG("actual format in use: ",
                actual_format));
        if (actual_format.mSampleRate == change_format.mSampleRate &&
            actual_format.mFormatID == change_format.mFormatID &&
            actual_format.mFramesPerPacket == change_format.mFramesPerPacket)
        {
            /* The right format is now active */
            retValue = true;
            break;
        }

        /* We need to check again */
    }

out:
    vlc_mutex_unlock(&w.lock);

    /* Removing the property listener */
    ret = AO_UPDATELISTENER(i_stream_id, false, StreamListener, &w,
                            kAudioStreamPropertyPhysicalFormat,
                            kAudioObjectPropertyScopeGlobal);
    if (ret != VLC_SUCCESS)
        retValue = false;

    return retValue;
}

#pragma mark -
#pragma mark core interaction

static int
SwitchAudioDevice(audio_output_t *p_aout, const char *name)
{
    aout_sys_t *p_sys = p_aout->sys;

    p_sys->i_new_selected_dev = (name) ? atoi(name) : 0;

    aout_DeviceReport(p_aout, name);
    aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);

    return 0;
}

static int
VolumeSet(audio_output_t * p_aout, float volume)
{
    aout_sys_t *p_sys = p_aout->sys;
    OSStatus err;

    if (p_sys->b_digital)
        return VLC_EGENERIC;

    p_sys->f_volume = volume;
    aout_VolumeReport(p_aout, volume);

    /* Set volume for output unit */
    err = AudioUnitSetParameter(p_sys->au_unit,
                                kHALOutputParam_Volume,
                                kAudioUnitScope_Global,
                                0,
                                volume * volume * volume,
                                0);

    if (var_InheritBool(p_aout, "volume-save"))
        config_PutInt("auhal-volume", lroundf(volume * AOUT_VOLUME_DEFAULT));

    return (err == noErr) ? VLC_SUCCESS : VLC_EGENERIC;
}

static int
MuteSet(audio_output_t * p_aout, bool mute)
{
    ca_MuteSet(p_aout, mute);
    aout_MuteReport(p_aout, mute);

    return VLC_SUCCESS;
}

#pragma mark -
#pragma mark actual playback

/*
 * RenderCallbackSPDIF: callback for SPDIF audio output
 */
static OSStatus
RenderCallbackSPDIF(AudioDeviceID inDevice, const AudioTimeStamp * inNow,
                    const AudioBufferList * inInputData,
                    const AudioTimeStamp * inInputTime,
                    AudioBufferList * outOutputData,
                    const AudioTimeStamp * inOutputTime, void *p_data)
{
    VLC_UNUSED(inNow);
    VLC_UNUSED(inDevice);
    VLC_UNUSED(inInputData);
    VLC_UNUSED(inInputTime);
    VLC_UNUSED(inOutputTime);

    audio_output_t * p_aout = p_data;
    aout_sys_t *p_sys = p_aout->sys;
    uint8_t *p_output = outOutputData->mBuffers[p_sys->i_stream_index].mData;
    size_t i_size = outOutputData->mBuffers[p_sys->i_stream_index].mDataByteSize;

    uint64_t i_host_time = (inOutputTime->mFlags & kAudioTimeStampHostTimeValid)
                         ? inOutputTime->mHostTime : 0;

    ca_Render(p_aout, i_host_time, p_output, i_size, NULL);

    return noErr;
}

#pragma mark -
#pragma mark initialization

static void
WarnConfiguration(audio_output_t *p_aout)
{
    aout_sys_t *p_sys = p_aout->sys;
    char *warned_devices = var_CreateGetNonEmptyString(p_aout, "auhal-warned-devices");
    bool dev_is_warned = false;
    unsigned dev_count = 0;

    /* Check if the actual device was already warned */
    if (warned_devices)
    {
        char *dup = strdup(warned_devices);
        if (dup)
        {
            char *savetpr;
            for (const char *dev = strtok_r(dup, ";", &savetpr);
                 dev != NULL && !dev_is_warned;
                 dev = strtok_r(NULL, ";", &savetpr))
            {
                dev_count++;
                int devid = atoi(dev);
                if (devid >= 0 && (unsigned) devid == p_sys->i_selected_dev)
                {
                    dev_is_warned = true;
                    break;
                }
            }
            free(dup);
        }
    }

    /* Warn only one time per device */
    if (!dev_is_warned)
    {
        msg_Warn(p_aout, "You should configure your speaker layout with "
                "Audio Midi Setup in /Applications/Utilities. VLC will "
                "output Stereo only.");
        vlc_dialog_display_error(p_aout,
            _("Audio device is not configured"), "%s",
            _("You should configure your speaker layout with "
            "\"Audio Midi Setup\" in /Applications/"
            "Utilities. VLC will output Stereo only."));

        /* Don't save too many devices */
        if (dev_count >= 10)
        {
            char *end = strrchr(warned_devices, ';');
            if (end)
                *end = 0;
        }

        /* Add the actual device to the list of warned devices */
        char *new_warned_devices;
        if (asprintf(&new_warned_devices, "%u%s%s", p_sys->i_selected_dev,
                     warned_devices ? ";" : "",
                     warned_devices ? warned_devices : "") != -1)
        {
            config_PutPsz("auhal-warned-devices", new_warned_devices);
            var_SetString(p_aout, "auhal-warned-devices", new_warned_devices);
            free(new_warned_devices);
        }
    }
    free(warned_devices);
}

/*
 * StartAnalog: open and setup a HAL AudioUnit to do PCM audio output
 */
static int
StartAnalog(audio_output_t *p_aout, audio_sample_format_t *fmt,
            vlc_tick_t latency_us)
{
    aout_sys_t                  *p_sys = p_aout->sys;
    OSStatus                    err = noErr;
    UInt32                      i_param_size;
    AudioChannelLayout          *layout = NULL;

    if (aout_FormatNbChannels(fmt) == 0)
        return VLC_EGENERIC;

    p_sys->au_unit = au_NewOutputInstance(p_aout, kAudioUnitSubType_HALOutput);
    if (p_sys->au_unit == NULL)
        return VLC_EGENERIC;

    p_aout->current_sink_info.headphones = AudioDeviceIsAHeadphone(p_aout, p_sys->i_selected_dev);

    /* Set the device we will use for this output unit */
    err = AudioUnitSetProperty(p_sys->au_unit,
                               kAudioOutputUnitProperty_CurrentDevice,
                               kAudioUnitScope_Global, 0,
                               &p_sys->i_selected_dev, sizeof(AudioObjectID));

    if (err != noErr)
    {
        ca_LogErr("cannot select audio output device, PCM output failed");
        goto error;
    }

    /* Get the channel layout of the device side of the unit (vlc -> unit ->
     * device) */
    err = AudioUnitGetPropertyInfo(p_sys->au_unit,
                                   kAudioDevicePropertyPreferredChannelLayout,
                                   kAudioUnitScope_Output, 0, &i_param_size,
                                   NULL);
    if (err == noErr)
    {
        layout = (AudioChannelLayout *)malloc(i_param_size);
        if (layout == NULL)
            goto error;

        OSStatus err =
            AudioUnitGetProperty(p_sys->au_unit,
                                 kAudioDevicePropertyPreferredChannelLayout,
                                 kAudioUnitScope_Output, 0, layout,
                                 &i_param_size);
        if (err != noErr)
            goto error;
    }
    else
        ca_LogWarn("device driver does not support "
                   "kAudioDevicePropertyPreferredChannelLayout - using stereo");

    /* Do the last VLC aout setups */
    bool warn_configuration;
    int ret = au_Initialize(p_aout, p_sys->au_unit, fmt, layout, latency_us,
                            NULL, &warn_configuration);
    if (ret != VLC_SUCCESS)
        goto error;

    err = AudioOutputUnitStart(p_sys->au_unit);
    if (err != noErr)
    {
        ca_LogErr("AudioUnitStart failed");
        au_Uninitialize(p_aout, p_sys->au_unit);
        goto error;
    }

    /* Set volume for output unit */
    VolumeSet(p_aout, p_sys->f_volume);

    free(layout);

    if (warn_configuration)
        WarnConfiguration(p_aout);

    return VLC_SUCCESS;
error:
    AudioComponentInstanceDispose(p_sys->au_unit);
    p_sys->au_unit = NULL;
    free(layout);
    return VLC_EGENERIC;
}

/*
 * StartSPDIF: Setup an encoded digital stream (SPDIF) output
 */
static int
StartSPDIF(audio_output_t * p_aout, audio_sample_format_t *fmt)
{
    aout_sys_t *p_sys = p_aout->sys;
    int ret;

    /* Check if device supports digital */
    if (!AudioDeviceSupportsDigital(p_aout, p_sys->i_selected_dev))
    {
        msg_Dbg(p_aout, "Audio device supports PCM mode only");
        return VLC_EGENERIC;
    }

    ret = AO_GET1PROP(p_sys->i_selected_dev, pid_t, &p_sys->i_hog_pid,
                      kAudioDevicePropertyHogMode,
                      kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
    {
        /* This is not a fatal error. Some drivers simply don't support this
         * property */
        p_sys->i_hog_pid = -1;
    }

    if (p_sys->i_hog_pid != -1 && p_sys->i_hog_pid != getpid())
    {
        msg_Err(p_aout, "Selected audio device is exclusively in use by another"
                " program.");
        vlc_dialog_display_error(p_aout, _("Audio output failed"), "%s",
            _("The selected audio output device is exclusively in "
            "use by another program."));
        return VLC_EGENERIC;
    }

    AudioStreamBasicDescription desired_stream_format;
    memset(&desired_stream_format, 0, sizeof(desired_stream_format));

    /* Start doing the SPDIF setup process */
    p_sys->b_digital = true;

    /* Hog the device */
    p_sys->i_hog_pid = getpid();

    /*
     * HACK: On 10.6, auhal will trigger the streams changed callback when
     * calling below line, directly in the same thread. This call needs to be
     * ignored to avoid endless restarting.
     */
    p_sys->b_ignore_streams_changed_callback = true;
    ret = AO_SETPROP(p_sys->i_selected_dev, sizeof(p_sys->i_hog_pid),
                     &p_sys->i_hog_pid, kAudioDevicePropertyHogMode,
                     kAudioObjectPropertyScopeOutput);
    p_sys->b_ignore_streams_changed_callback = false;

    if (ret != VLC_SUCCESS)
        return ret;

    if (AO_HASPROP(p_sys->i_selected_dev, kAudioDevicePropertySupportsMixing,
                   kAudioObjectPropertyScopeGlobal))
    {
        /* Set mixable to false if we are allowed to */
        UInt32 b_mix;
        bool b_writeable = AO_ISPROPSETTABLE(p_sys->i_selected_dev,
                                             kAudioDevicePropertySupportsMixing,
                                             kAudioObjectPropertyScopeGlobal);

        ret = AO_GET1PROP(p_sys->i_selected_dev, UInt32, &b_mix,
                          kAudioDevicePropertySupportsMixing,
                          kAudioObjectPropertyScopeGlobal);
        if (ret == VLC_SUCCESS && b_writeable)
        {
            b_mix = 0;
            ret = AO_SETPROP(p_sys->i_selected_dev, sizeof(UInt32), &b_mix,
                             kAudioDevicePropertySupportsMixing,
                             kAudioObjectPropertyScopeGlobal);
            p_sys->b_changed_mixing = true;
        }

        if (ret != VLC_SUCCESS)
        {
            msg_Err(p_aout, "failed to set mixmode");
            return ret;
        }
    }

    /* Get a list of all the streams on this device */
    size_t i_streams;
    AudioStreamID *p_streams;
    ret = AO_GETPROP(p_sys->i_selected_dev, AudioStreamID, &i_streams,
                     &p_streams, kAudioDevicePropertyStreams,
                     kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
        return ret;

    for (unsigned i = 0; i < i_streams && p_sys->i_stream_index < 0 ; i++)
    {
        /* Find a stream with a cac3 stream */
        bool b_digital = false;

        /* Retrieve all the stream formats supported by each output stream */
        size_t i_formats;
        AudioStreamRangedDescription *p_format_list;
        int ret = AO_GETPROP(p_streams[i], AudioStreamRangedDescription,
                             &i_formats, &p_format_list,
                             kAudioStreamPropertyAvailablePhysicalFormats,
                             kAudioObjectPropertyScopeGlobal);
        if (ret != VLC_SUCCESS)
            continue;

        /* Check if one of the supported formats is a digital format */
        for (size_t j = 0; j < i_formats; j++)
        {
            if (IsAudioFormatDigital(p_format_list[j].mFormat.mFormatID))
            {
                b_digital = true;
                break;
            }
        }

        if (b_digital)
        {
            /* if this stream supports a digital (cac3) format, then go set it.
             * */
            int i_requested_rate_format = -1;
            int i_current_rate_format = -1;
            int i_backup_rate_format = -1;

            if (!p_sys->b_revert)
            {
                /* Retrieve the original format of this stream first if not
                 * done so already */
                AudioStreamBasicDescription current_streamformat;
                int ret = AO_GET1PROP(p_streams[i], AudioStreamBasicDescription,
                                      &current_streamformat,
                                      kAudioStreamPropertyPhysicalFormat,
                                      kAudioObjectPropertyScopeGlobal);
                if (ret != VLC_SUCCESS)
                    continue;

                /*
                 * Only the first found format id is accepted. In case of
                 * another id later on, we still use the already saved one.
                 * This can happen if the user plugs in a spdif cable while a
                 * stream is already playing. Then, auhal already misleadingly
                 * reports an ac3 format here whereas the original format
                 * should be still pcm.
                 */
                if (p_sys->sfmt_revert.mFormatID > 0
                 && p_sys->sfmt_revert.mFormatID != current_streamformat.mFormatID
                 && p_streams[i] == p_sys->i_stream_id)
                {
                    msg_Warn(p_aout, STREAM_FORMAT_MSG("Detected current stream"
                             " format: ", current_streamformat));
                    msg_Warn(p_aout, "... there is another stream format "
                             "already stored, the current one is ignored");
                }
                else
                    p_sys->sfmt_revert = current_streamformat;

                p_sys->b_revert = true;
            }

            p_sys->i_stream_id = p_streams[i];
            p_sys->i_stream_index = i;

            for (size_t j = 0; j < i_formats; j++)
            {
                if (IsAudioFormatDigital(p_format_list[j].mFormat.mFormatID))
                {
                    if (p_format_list[j].mFormat.mSampleRate == fmt->i_rate)
                    {
                        i_requested_rate_format = j;
                        break;
                    }
                    else if (p_format_list[j].mFormat.mSampleRate ==
                             p_sys->sfmt_revert.mSampleRate)
                        i_current_rate_format = j;
                    else
                    {
                        if (i_backup_rate_format < 0
                         || p_format_list[j].mFormat.mSampleRate >
                            p_format_list[i_backup_rate_format].mFormat.mSampleRate)
                            i_backup_rate_format = j;
                    }
                }

            }

            if (i_requested_rate_format >= 0)
            {
                /* We prefer to output at the samplerate of the original audio */
                desired_stream_format =
                    p_format_list[i_requested_rate_format].mFormat;
            }
            else if (i_current_rate_format >= 0)
            {
                /* If not possible, we will try to use the current samplerate
                 * of the device */
                desired_stream_format =
                    p_format_list[i_current_rate_format].mFormat;
            }
            else
            {
                /* And if we have to, any digital format will be just fine
                 * (highest rate possible) */
                desired_stream_format =
                    p_format_list[i_backup_rate_format].mFormat;
            }
        }
        free(p_format_list);
    }
    free(p_streams);

    msg_Dbg(p_aout, STREAM_FORMAT_MSG("original stream format: ",
            p_sys->sfmt_revert));

    if (!AudioStreamChangeFormat(p_aout, p_sys->i_stream_id, desired_stream_format))
    {
        msg_Err(p_aout, "failed to change stream format for SPDIF output");
        return VLC_EGENERIC;
    }

    /* Set the format flags */
    if (desired_stream_format.mFormatFlags & kAudioFormatFlagIsBigEndian)
        fmt->i_format = VLC_CODEC_SPDIFB;
    else
        fmt->i_format = VLC_CODEC_SPDIFL;
    fmt->i_bytes_per_frame = AOUT_SPDIF_SIZE;
    fmt->i_frame_length = A52_FRAME_NB;
    fmt->i_rate = (unsigned int)desired_stream_format.mSampleRate;
    aout_FormatPrepare(fmt);

    /* Add IOProc callback */
    OSStatus err =
        AudioDeviceCreateIOProcID(p_sys->i_selected_dev,
                                  RenderCallbackSPDIF,
                                  p_aout, &p_sys->i_procID);
    if (err != noErr)
    {
        ca_LogErr("Failed to create Process ID");
        return VLC_EGENERIC;
    }

    ret = ca_Initialize(p_aout, fmt, 0, NULL);
    if (ret != VLC_SUCCESS)
    {
        AudioDeviceDestroyIOProcID(p_sys->i_selected_dev, p_sys->i_procID);
        return VLC_EGENERIC;
    }

    /* Start device */
    err = AudioDeviceStart(p_sys->i_selected_dev, p_sys->i_procID);
    if (err != noErr)
    {
        ca_LogErr("Failed to start audio device");

        err = AudioDeviceDestroyIOProcID(p_sys->i_selected_dev, p_sys->i_procID);
        if (err != noErr)
            ca_LogErr("Failed to destroy process ID");

        return VLC_EGENERIC;
    }

    msg_Dbg(p_aout, "Using audio device for digital output");
    return VLC_SUCCESS;
}

static void
Stop(audio_output_t *p_aout)
{
    aout_sys_t          *p_sys = p_aout->sys;
    OSStatus            err = noErr;

    msg_Dbg(p_aout, "Stopping the auhal module");

    if (p_sys->au_unit)
    {
        AudioOutputUnitStop(p_sys->au_unit);
        au_Uninitialize(p_aout, p_sys->au_unit);
        AudioComponentInstanceDispose(p_sys->au_unit);
        p_sys->au_unit = NULL;
    }
    else
    {
        assert(p_sys->b_digital);

        /* Stop device */
        err = AudioDeviceStop(p_sys->i_selected_dev,
                               p_sys->i_procID);
        if (err != noErr)
            ca_LogErr("AudioDeviceStop failed");

        /* Remove IOProc callback */
        err = AudioDeviceDestroyIOProcID(p_sys->i_selected_dev,
                                          p_sys->i_procID);
        if (err != noErr)
            ca_LogErr("Failed to destroy Process ID");

        if (p_sys->b_revert
         && !AudioStreamChangeFormat(p_aout, p_sys->i_stream_id, p_sys->sfmt_revert))
            msg_Err(p_aout, "failed to revert stream format in close");

        if (p_sys->b_changed_mixing
         && p_sys->sfmt_revert.mFormatID != kAudioFormat60958AC3)
        {
            UInt32 b_mix;
            /* Revert mixable to true if we are allowed to */
            bool b_writeable =
                AO_ISPROPSETTABLE(p_sys->i_selected_dev,
                                  kAudioDevicePropertySupportsMixing,
                                  kAudioObjectPropertyScopeGlobal);
            int ret = AO_GET1PROP(p_sys->i_selected_dev, UInt32, &b_mix,
                                  kAudioDevicePropertySupportsMixing,
                                  kAudioObjectPropertyScopeOutput);
            if (ret == VLC_SUCCESS && b_writeable)
            {
                msg_Dbg(p_aout, "mixable is: %d", b_mix);
                b_mix = 1;
                ret = AO_SETPROP(p_sys->i_selected_dev,
                                 sizeof(UInt32), &b_mix,
                                 kAudioDevicePropertySupportsMixing,
                                 kAudioObjectPropertyScopeOutput);
            }

            if (ret != VLC_SUCCESS)
                msg_Err(p_aout, "failed to re-set mixmode");
        }
        ca_Uninitialize(p_aout);

        if (p_sys->i_hog_pid == getpid())
        {
            p_sys->i_hog_pid = -1;

            /*
             * HACK: On 10.6, auhal will trigger the streams changed callback
             * when calling below line, directly in the same thread. This call
             * needs to be ignored to avoid endless restarting.
             */
            p_sys->b_ignore_streams_changed_callback = true;
            AO_SETPROP(p_sys->i_selected_dev, sizeof(p_sys->i_hog_pid),
                       &p_sys->i_hog_pid, kAudioDevicePropertyHogMode,
                       kAudioObjectPropertyScopeOutput);
            p_sys->b_ignore_streams_changed_callback = false;
        }

        p_sys->b_digital = false;
    }

    /* remove audio device alive callback */
    AO_UPDATELISTENER(p_sys->i_selected_dev, false, DeviceAliveListener, p_aout,
                      kAudioDevicePropertyDeviceIsAlive,
                      kAudioObjectPropertyScopeGlobal);
}

static vlc_tick_t
GetLatency(audio_output_t *p_aout, const audio_sample_format_t *fmt)
{
    aout_sys_t *p_sys = p_aout->sys;

    UInt32 i_latency_samples;
    vlc_tick_t i_latency_us, i_device_latency_us = 0, i_device_offset_us = 0,
               i_stream_latency_us = 0;
    /* Get device latency */
    int ret = AO_GET1PROP(p_sys->i_selected_dev, UInt32, &i_latency_samples,
                          kAudioDevicePropertyLatency,
                          kAudioObjectPropertyScopeOutput);
    if (ret == VLC_SUCCESS)
        i_device_latency_us = vlc_tick_from_samples(i_latency_samples, fmt->i_rate);
    else
        msg_Warn(p_aout, "failed to get kAudioDevicePropertyLatency");

    /* Get device safety offset */
    ret = AO_GET1PROP(p_sys->i_selected_dev, UInt32, &i_latency_samples,
                      kAudioDevicePropertySafetyOffset,
                      kAudioObjectPropertyScopeOutput);
    if (ret == VLC_SUCCESS)
        i_device_offset_us = vlc_tick_from_samples(i_latency_samples, fmt->i_rate);
    else
        msg_Warn(p_aout, "failed to get kAudioDevicePropertySafetyOffset");

    AudioStreamID sid;
    /* Get stream latency */
    ret = AO_GET1PROP(p_sys->i_selected_dev, AudioStreamID, &sid,
                      kAudioDevicePropertyStreams, kAudioObjectPropertyScopeOutput);
    if (ret == VLC_SUCCESS)
    {
        ret = AO_GET1PROP(sid, UInt32, &i_latency_samples,
                          kAudioStreamPropertyLatency,
                          kAudioObjectPropertyScopeOutput);
        if (ret == VLC_SUCCESS)
            i_stream_latency_us = vlc_tick_from_samples(i_latency_samples, fmt->i_rate);
        else
            msg_Warn(p_aout, "failed to get kAudioStreamPropertyLatency");
    }
    else
        msg_Warn(p_aout, "failed to get kAudioDevicePropertyStreams");

    i_latency_us = i_device_latency_us + i_device_offset_us + i_stream_latency_us;

    msg_Dbg(p_aout, "Current device has a latency of %" PRId64 " us "
            "(device: %" PRId64 " us, offset: %" PRId64 " us, stream: %" PRId64 " us)", i_latency_us,
            i_device_latency_us, i_device_offset_us, i_stream_latency_us);

    return i_latency_us;
}

static int
Start(audio_output_t *p_aout, audio_sample_format_t *restrict fmt)
{
    aout_sys_t              *p_sys = NULL;

    /* Use int here, to match kAudioDevicePropertyDeviceIsAlive
     * property size */
    int                     b_alive = false;

    if (AOUT_FMT_HDMI(fmt))
        return VLC_EGENERIC;

    p_sys = p_aout->sys;
    p_sys->i_stream_index = -1;
    p_sys->b_revert = false;
    p_sys->b_changed_mixing = false;

    vlc_mutex_lock(&p_sys->selected_device_lock);
    bool do_spdif;
    if (AOUT_FMT_SPDIF (fmt))
    {
        if (!(p_sys->i_new_selected_dev & AOUT_VAR_SPDIF_FLAG))
        {
            vlc_mutex_unlock(&p_sys->selected_device_lock);
            return VLC_EGENERIC;
        }
        do_spdif = true;
    }
    else
        do_spdif = false;

    p_sys->i_selected_dev = p_sys->i_new_selected_dev & ~AOUT_VAR_SPDIF_FLAG;

    aout_FormatPrint(p_aout, "VLC is looking for:", fmt);

    msg_Dbg(p_aout, "attempting to use device %i", p_sys->i_selected_dev);

    if (p_sys->i_selected_dev > 0)
    {
        /* Check if device is in devices list. Only checking for
         * kAudioDevicePropertyDeviceIsAlive is not sufficient, as a former
         * airplay device might be already gone, but the device number might be
         * still valid. Core Audio even says that this device would be alive.
         * Don't ask why, its Core Audio. */
        CFIndex count = CFArrayGetCount(p_sys->device_list);
        CFNumberRef deviceNumber =
            CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type,
                           &p_sys->i_selected_dev);
        if (CFArrayContainsValue(p_sys->device_list,
                                 CFRangeMake(0, count), deviceNumber))
        {
            /* Check if the desired device is alive and usable */
            int ret = AO_GET1PROP(p_sys->i_selected_dev, int, &b_alive,
                                  kAudioDevicePropertyDeviceIsAlive,
                                  kAudioObjectPropertyScopeGlobal);
            if (ret != VLC_SUCCESS)
                b_alive = false; /* Be tolerant */

            if (!b_alive)
                msg_Warn(p_aout, "selected audio device is not alive, switching"
                         " to default device");

        }
        else
        {
            msg_Warn(p_aout, "device id %i not found in the current devices "
                     "list, fallback to default device", p_sys->i_selected_dev);
        }
        CFRelease(deviceNumber);
    }

    p_sys->b_selected_dev_is_default = false;
    if (!b_alive || p_sys->i_selected_dev == 0)
    {
        p_sys->b_selected_dev_is_default = true;

        AudioObjectID defaultDeviceID = 0;
        int ret = AO_GET1PROP(kAudioObjectSystemObject, AudioObjectID,
                              &defaultDeviceID,
                              kAudioHardwarePropertyDefaultOutputDevice,
                              kAudioObjectPropertyScopeOutput);
        if (ret != VLC_SUCCESS)
        {
            vlc_mutex_unlock(&p_sys->selected_device_lock);
            return VLC_EGENERIC;
        }
        else
            msg_Dbg(p_aout, "using default audio device %i", defaultDeviceID);

        p_sys->i_selected_dev = defaultDeviceID;
    }
    vlc_mutex_unlock(&p_sys->selected_device_lock);

    /* add a callback to see if the device dies later on */
    AO_UPDATELISTENER(p_sys->i_selected_dev, true, DeviceAliveListener, p_aout,
                      kAudioDevicePropertyDeviceIsAlive,
                      kAudioObjectPropertyScopeGlobal);

    vlc_tick_t i_latency_us = GetLatency(p_aout, fmt);

    /* Check for Digital mode or Analog output mode */
    if (do_spdif)
    {
        if (StartSPDIF(p_aout, fmt) == VLC_SUCCESS)
        {
            msg_Dbg(p_aout, "digital output successfully opened");
            return VLC_SUCCESS;
        }
    }
    else
    {
        if (StartAnalog(p_aout, fmt, i_latency_us) == VLC_SUCCESS)
        {
            msg_Dbg(p_aout, "analog output successfully opened");
            fmt->channel_type = AUDIO_CHANNEL_TYPE_BITMAP;
            return VLC_SUCCESS;
        }
    }

    msg_Err(p_aout, "opening auhal output failed");
    AO_UPDATELISTENER(p_sys->i_selected_dev, false, DeviceAliveListener, p_aout,
                      kAudioDevicePropertyDeviceIsAlive,
                      kAudioObjectPropertyScopeGlobal);
    return VLC_EGENERIC;
}

static void Close(vlc_object_t *obj)
{
    audio_output_t *p_aout = (audio_output_t *)obj;
    aout_sys_t *p_sys = p_aout->sys;

    /* remove audio devices callback */
    AO_UPDATELISTENER(kAudioObjectSystemObject, false, DevicesListener, p_aout,
                      kAudioHardwarePropertyDevices,
                      kAudioObjectPropertyScopeGlobal);

    /* remove listener to be notified about changes in default audio device */
    AO_UPDATELISTENER(kAudioObjectSystemObject, false,
                      DefaultDeviceChangedListener, p_aout,
                      kAudioHardwarePropertyDefaultOutputDevice,
                      kAudioObjectPropertyScopeGlobal);

    /*
     * StreamsChangedListener can rebuild the device list and thus held the
     * device_list_lock.  To avoid a possible deadlock, an array copy is
     * created here.  In rare cases, this can lead to missing
     * StreamsChangedListener callback deregistration (TODO).
     */
    vlc_mutex_lock(&p_sys->device_list_lock);
    CFArrayRef device_list_cpy = CFArrayCreateCopy(NULL, p_sys->device_list);
    vlc_mutex_unlock(&p_sys->device_list_lock);

    /* remove streams callbacks */
    CFIndex count = CFArrayGetCount(device_list_cpy);
    if (count > 0)
    {
        for (CFIndex x = 0; x < count; x++)
        {
            AudioDeviceID deviceId = 0;
            CFNumberRef cfn_device_id =
                CFArrayGetValueAtIndex(device_list_cpy, x);
            if (!cfn_device_id)
                continue;

            CFNumberGetValue(cfn_device_id, kCFNumberSInt32Type, &deviceId);
            if (!(deviceId & AOUT_VAR_SPDIF_FLAG))
                ManageAudioStreamsCallback(p_aout, deviceId, false);
        }
    }

    CFRelease(device_list_cpy);
    CFRelease(p_sys->device_list);

    char *psz_device = aout_DeviceGet(p_aout);
    config_PutPsz("auhal-audio-device", psz_device);
    free(psz_device);

    free(p_sys);
}

static int Open(vlc_object_t *obj)
{
    audio_output_t *p_aout = (audio_output_t *)obj;

    aout_sys_t *p_sys = p_aout->sys = calloc(1, sizeof (*p_sys));
    if (unlikely(p_sys == NULL))
        return VLC_ENOMEM;

    if (ca_Open(p_aout) != VLC_SUCCESS)
    {
        free(p_sys);
        return VLC_EGENERIC;
    }

    vlc_mutex_init(&p_sys->device_list_lock);
    vlc_mutex_init(&p_sys->selected_device_lock);
    p_sys->b_digital = false;
    p_sys->au_unit = NULL;
    p_sys->b_ignore_streams_changed_callback = false;
    p_sys->b_selected_dev_is_default = false;
    memset(&p_sys->sfmt_revert, 0, sizeof(p_sys->sfmt_revert));
    p_sys->i_stream_id = 0;

    p_aout->start = Start;
    p_aout->stop = Stop;
    p_aout->volume_set = VolumeSet;
    p_aout->mute_set = MuteSet;
    p_aout->device_select = SwitchAudioDevice;
    p_sys->device_list = CFArrayCreate(kCFAllocatorDefault, NULL, 0, NULL);

    /*
     * Force an own run loop for callbacks.
     *
     * According to rtaudio, this is absolutely necessary since 10.6 to get
     * correct notifications.  It might fix issues when using the module as a
     * library where a proper loop is not setup already.
     */
    CFRunLoopRef theRunLoop = NULL;
    AO_SETPROP(kAudioObjectSystemObject, sizeof(CFRunLoopRef),
               &theRunLoop, kAudioHardwarePropertyRunLoop,
               kAudioObjectPropertyScopeGlobal);

    /* Attach a listener so that we are notified of a change in the device
     * setup */
    AO_UPDATELISTENER(kAudioObjectSystemObject, true, DevicesListener, p_aout,
                      kAudioHardwarePropertyDevices,
                      kAudioObjectPropertyScopeGlobal);

    /* Attach a listener to be notified about changes in default audio device */
    AO_UPDATELISTENER(kAudioObjectSystemObject, true,
                      DefaultDeviceChangedListener, p_aout,
                      kAudioHardwarePropertyDefaultOutputDevice,
                      kAudioObjectPropertyScopeGlobal);

    char *psz_audio_device = var_InheritString(p_aout, "auhal-audio-device");
    if (psz_audio_device != NULL)
    {
        int dev_id_int = atoi(psz_audio_device);
        UInt32 dev_id = dev_id_int < 0 ? 0 : dev_id_int;

        bool isDigital = (dev_id & AOUT_VAR_SPDIF_FLAG) != 0;
        msg_Dbg(obj, "Trying to use stored audio device %d (%s)",
                (dev_id & ~AOUT_VAR_SPDIF_FLAG), isDigital ? "digital" : "analog");

        RebuildDeviceList(p_aout, &dev_id);
        p_sys->i_new_selected_dev = dev_id;
        free(psz_audio_device);
    }
    else
    {
        RebuildDeviceList(p_aout, NULL);
        p_sys->i_new_selected_dev = 0;
    }

    char deviceid[10];
    sprintf(deviceid, "%i", p_sys->i_new_selected_dev);
    aout_DeviceReport(p_aout, deviceid);

    /* remember the volume */
    p_sys->f_volume = var_InheritInteger(p_aout, "auhal-volume")
                    / (float)AOUT_VOLUME_DEFAULT;
    aout_VolumeReport(p_aout, p_sys->f_volume);
    MuteSet(p_aout, var_InheritBool(p_aout, "mute"));

    return VLC_SUCCESS;
}
