auhal.c 56.2 KB
Newer Older
1
/*****************************************************************************
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
2
 * auhal.c: AUHAL and Coreaudio output plugin
3
 *****************************************************************************
4
 * Copyright (C) 2005 - 2017 VLC authors and VideoLAN
5 6
 *
 * Authors: Derk-Jan Hartman <hartman at videolan dot org>
7
 *          Felix Paul Kühne <fkuehne at videolan dot org>
8
 *          David Fuhrmann <david dot fuhrmann at googlemail dot com>
9
 *
Jean-Baptiste Kempf's avatar
LGPL  
Jean-Baptiste Kempf committed
10 11 12
 * 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
13
 * (at your option) any later version.
14
 *
15 16
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Jean-Baptiste Kempf's avatar
LGPL  
Jean-Baptiste Kempf committed
17 18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
19
 *
Jean-Baptiste Kempf's avatar
LGPL  
Jean-Baptiste Kempf committed
20
 * You should have received a copy of the GNU Lesser General Public License
21 22
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
23 24
 *****************************************************************************/

25
#pragma mark includes
26

27
#import "coreaudio_common.h"
28

29
#import <vlc_plugin.h>
Thomas Guillem's avatar
Thomas Guillem committed
30
#import <vlc_dialog.h>                      // vlc_dialog_display_error
31

32
#import <CoreAudio/CoreAudio.h>             // AudioDeviceID
33
#import <CoreServices/CoreServices.h>
34

35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
#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 VOLUME_LONGTEXT VOLUME_TEXT

#define DEVICE_TEXT N_("Last audio device")
#define DEVICE_LONGTEXT DEVICE_TEXT

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_category(CAT_AUDIO)
    set_subcategory(SUBCAT_AUDIO_AOUT)
    set_callbacks(Open, Close)
    add_integer("auhal-volume", AOUT_VOLUME_DEFAULT,
                VOLUME_TEXT, VOLUME_LONGTEXT, true)
    change_integer_range(0, AOUT_VOLUME_MAX)
    add_string("auhal-audio-device", "", DEVICE_TEXT, DEVICE_LONGTEXT, true)
    add_obsolete_integer("macosx-audio-device") /* since 2.1.0 */
vlc_module_end ()

65 66 67
#pragma mark -
#pragma mark private declarations

68
#define AOUT_VAR_SPDIF_FLAG 0xf00000
69 70 71 72 73 74 75 76 77

/*****************************************************************************
 * 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.
 *****************************************************************************/
struct aout_sys_t
{
78 79
    struct aout_sys_common c;

Thomas Guillem's avatar
Thomas Guillem committed
80 81 82 83 84 85
    /* 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;
86

Thomas Guillem's avatar
Thomas Guillem committed
87 88 89 90
    /* DeviceID of current device */
    AudioDeviceIOProcID         i_procID;
    /* Are we running in digital mode? */
    bool                        b_digital;
91

92
    /* AUHAL specific */
Thomas Guillem's avatar
Thomas Guillem committed
93
    AudioUnit                   au_unit;
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
94

95
    /* CoreAudio SPDIF mode specific */
Thomas Guillem's avatar
Thomas Guillem committed
96 97 98 99 100 101 102 103 104 105 106 107 108
    /* 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;

109
    CFArrayRef                  device_list;
Thomas Guillem's avatar
Thomas Guillem committed
110 111
    /* protects access to device_list */
    vlc_mutex_t                 device_list_lock;
112

Thomas Guillem's avatar
Thomas Guillem committed
113 114 115 116 117
    /* 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;
118

119 120
    float                       f_volume;
    bool                        b_mute;
121 122

    bool                        b_ignore_streams_changed_callback;
123 124
};

125
#pragma mark -
126
#pragma mark helpers
127

128 129 130 131 132 133 134
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);
135

136 137 138 139 140 141 142
    /* 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, "
143 144
                "prop: [%4.4s], OSStatus: %d", id, (const char *) &p_address[0],
                (int)err);
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
        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, "
191 192
                "prop: [%4.4s], OSStatus: %d", id, (const char *) &p_address[0],
                (int) err);
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217

        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)
218
{
219 220 221 222 223
    Boolean b_settable;
    OSStatus err = AudioObjectIsPropertySettable(id, p_address, &b_settable);
    if (err != noErr)
    {
        msg_Warn(p_aout, "AudioObjectIsPropertySettable failed, device id: %i, "
224 225
                 "prop: [%4.4s], OSStatus: %d", id, (const char *)&p_address[0],
                 (int)err);
226 227 228 229
        return false;
    }
    return b_settable;
}
230

231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
#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, "
249 250
                 "prop: [%4.4s], OSStatus: %d", id, (const char *)&p_address[0],
                 (int)err);
251 252
        return VLC_EGENERIC;
    }
253 254
    return VLC_SUCCESS;
}
255

256 257 258 259 260 261 262 263 264 265 266 267 268
#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);
269

Thomas Guillem's avatar
Thomas Guillem committed
270 271
    if (err != noErr)
    {
272
        msg_Err(p_aout, "AudioObject%sPropertyListener failed, device id %i, "
273 274
                "prop: [%4.4s], OSStatus: %d", add ? "Add" : "Remove", id,
                (const char *)&p_address[0], (int)err);
275 276
        return VLC_EGENERIC;
    }
277 278
    return VLC_SUCCESS;
}
279

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

285 286 287
#pragma mark -
#pragma mark Stream / Hardware Listeners

288 289 290 291 292 293 294 295 296
static bool
IsAudioFormatDigital(AudioFormatID id)
{
    switch (id)
    {
        case 'IAC3':
        case 'iac3':
        case kAudioFormat60958AC3:
        case kAudioFormatAC3:
297
        case kAudioFormatEnhancedAC3:
298 299 300 301 302 303
            return true;
        default:
            return false;
    }
}

Thomas Guillem's avatar
Thomas Guillem committed
304 305 306
static OSStatus
StreamsChangedListener(AudioObjectID, UInt32,
                       const AudioObjectPropertyAddress [], void *);
307

Thomas Guillem's avatar
Thomas Guillem committed
308 309 310
static int
ManageAudioStreamsCallback(audio_output_t *p_aout, AudioDeviceID i_dev_id,
                           bool b_register)
311 312 313 314 315 316 317 318 319 320
{
    /* 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;

Thomas Guillem's avatar
Thomas Guillem committed
321 322
    for (size_t i = 0; i < i_streams; i++)
    {
323 324 325 326
        /* get notified when physical formats change */
        AO_UPDATELISTENER(p_streams[i], b_register, StreamsChangedListener,
                          p_aout, kAudioStreamPropertyAvailablePhysicalFormats,
                          kAudioObjectPropertyScopeGlobal);
327
    }
328

329 330 331
    free(p_streams);
    return VLC_SUCCESS;
}
332

333
/*
Thomas Guillem's avatar
Thomas Guillem committed
334 335
 * AudioStreamSupportsDigital: Checks if audio stream is compatible with raw
 * bitstreams
336
 */
337
static bool
Thomas Guillem's avatar
Thomas Guillem committed
338
AudioStreamSupportsDigital(audio_output_t *p_aout, AudioStreamID i_stream_id)
339
{
340
    bool b_return = false;
341

342
    /* Retrieve all the stream formats supported by each output stream */
343 344 345 346 347 348 349
    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)
350
        return false;
351

Thomas Guillem's avatar
Thomas Guillem committed
352 353
    for (size_t i = 0; i < i_formats; i++)
    {
354
#ifndef NDEBUG
Thomas Guillem's avatar
Thomas Guillem committed
355 356
        msg_Dbg(p_aout, STREAM_FORMAT_MSG("supported format: ",
                p_format_list[i].mFormat));
357
#endif
358

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

363 364 365
    free(p_format_list);
    return b_return;
}
366

367 368 369
/*
 * AudioDeviceSupportsDigital: Checks if device supports raw bitstreams
 */
370
static bool
Thomas Guillem's avatar
Thomas Guillem committed
371
AudioDeviceSupportsDigital(audio_output_t *p_aout, AudioDeviceID i_dev_id)
372
{
373 374 375 376 377 378
    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)
379
        return false;
380 381 382

    for (size_t i = 0; i < i_streams; i++)
    {
383
        if (AudioStreamSupportsDigital(p_aout, p_streams[i]))
384 385 386 387
        {
            free(p_streams);
            return true;
        }
388 389 390
    }

    free(p_streams);
391
    return false;
392 393
}

Thomas Guillem's avatar
Thomas Guillem committed
394 395
static void
ReportDevice(audio_output_t *p_aout, UInt32 i_id, char *name)
396
{
397 398
    char deviceid[10];
    sprintf(deviceid, "%i", i_id);
399

400 401
    aout_HotplugReport(p_aout, deviceid, name);
}
402

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
/*
 * AudioDeviceIsAHeadphone: Checks if device is a headphone
 */

static bool
AudioDeviceIsAHeadphone(audio_output_t *p_aout, AudioDeviceID i_dev_id)
{
    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
     'ispk' == internal speaker
     '61pd' == HDMI
     '    ' == Bluetooth accessory or AirPlay
    */

    return data == 'hdpn';
}

439 440 441
/*
 * AudioDeviceHasOutput: Checks if the device is actually an output device
 */
Thomas Guillem's avatar
Thomas Guillem committed
442 443
static int
AudioDeviceHasOutput(audio_output_t *p_aout, AudioDeviceID i_dev_id)
444
{
445 446 447 448 449
    size_t i_streams;
    int ret = AO_GETPROP(i_dev_id, AudioStreamID, &i_streams, NULL,
                          kAudioDevicePropertyStreams,
                          kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS || i_streams == 0)
450
        return FALSE;
451

452
    return TRUE;
453 454
}

Thomas Guillem's avatar
Thomas Guillem committed
455
static void
456
RebuildDeviceList(audio_output_t * p_aout, UInt32 *p_id_exists)
457
{
458
    struct aout_sys_t   *p_sys = p_aout->sys;
459

460
    msg_Dbg(p_aout, "Rebuild device list");
461

462
    ReportDevice(p_aout, 0, _("System Sound Output Device"));
Thomas Guillem's avatar
Thomas Guillem committed
463

464
    /* Get number of devices */
465 466 467 468 469 470 471 472
    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)
    {
473 474 475
        msg_Err(p_aout, "No audio output devices found.");
        return;
    }
476
    msg_Dbg(p_aout, "found %zu audio device(s)", i_devices);
477

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

482 483 484 485 486 487 488 489 490
    UInt32 i_id_exists;
    if (p_id_exists)
    {
        i_id_exists = *p_id_exists;
        *p_id_exists = 0;
    }
    else
        i_id_exists = 0;

Thomas Guillem's avatar
Thomas Guillem committed
491 492
    for (size_t i = 0; i < i_devices; i++)
    {
493 494 495
        CFStringRef device_name_ref;
        char *psz_name;
        CFIndex length;
496
        UInt32 i_id = p_devices[i];
497

498 499 500 501 502 503
        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);
504
            continue;
505
        }
506

507 508 509
        length = CFStringGetLength(device_name_ref);
        length++;
        psz_name = malloc(length);
Thomas Guillem's avatar
Thomas Guillem committed
510 511
        if (!psz_name)
        {
512 513 514
            CFRelease(device_name_ref);
            return;
        }
Thomas Guillem's avatar
Thomas Guillem committed
515 516
        CFStringGetCString(device_name_ref, psz_name, length,
                           kCFStringEncodingUTF8);
517
        CFRelease(device_name_ref);
518

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

Thomas Guillem's avatar
Thomas Guillem committed
521 522
        if (!AudioDeviceHasOutput(p_aout, i_id))
        {
523 524 525 526
            msg_Dbg(p_aout, "this '%s' is INPUT only. skipping...", psz_name);
            free(psz_name);
            continue;
        }
527

528 529 530
        if (p_id_exists && i_id == i_id_exists)
            *p_id_exists = i_id;

531
        ReportDevice(p_aout, i_id, psz_name);
Thomas Guillem's avatar
Thomas Guillem committed
532 533
        CFNumberRef deviceNumber = CFNumberCreate(kCFAllocatorDefault,
                                                  kCFNumberSInt32Type, &i_id);
534 535
        CFArrayAppendValue(currentListOfDevices, deviceNumber);
        CFRelease(deviceNumber);
536

Thomas Guillem's avatar
Thomas Guillem committed
537 538
        if (AudioDeviceSupportsDigital(p_aout, i_id))
        {
539 540 541 542 543
            msg_Dbg(p_aout, "'%s' supports digital output", psz_name);
            char *psz_encoded_name = nil;
            asprintf(&psz_encoded_name, _("%s (Encoded Output)"), psz_name);
            i_id = i_id | AOUT_VAR_SPDIF_FLAG;
            ReportDevice(p_aout, i_id, psz_encoded_name);
Thomas Guillem's avatar
Thomas Guillem committed
544 545
            deviceNumber = CFNumberCreate(kCFAllocatorDefault,
                                          kCFNumberSInt32Type, &i_id);
546 547 548 549
            CFArrayAppendValue(currentListOfDevices, deviceNumber);
            CFRelease(deviceNumber);
            free(psz_encoded_name);
        }
550

551
        // TODO: only register once for each device
552
        ManageAudioStreamsCallback(p_aout, p_devices[i], true);
553

554
        free(psz_name);
555 556
    }

557 558 559 560
    vlc_mutex_lock(&p_sys->device_list_lock);
    CFIndex count = 0;
    if (p_sys->device_list)
        count = CFArrayGetCount(p_sys->device_list);
Thomas Guillem's avatar
Thomas Guillem committed
561 562
    CFRange newListSearchRange =
        CFRangeMake(0, CFArrayGetCount(currentListOfDevices));
563

Thomas Guillem's avatar
Thomas Guillem committed
564 565
    if (count > 0)
    {
566 567 568
        msg_Dbg(p_aout, "Looking for removed devices");
        CFNumberRef cfn_device_id;
        int i_device_id = 0;
Thomas Guillem's avatar
Thomas Guillem committed
569 570 571 572 573 574
        for (CFIndex x = 0; x < count; x++)
        {
            if (!CFArrayContainsValue(currentListOfDevices, newListSearchRange,
                                      CFArrayGetValueAtIndex(p_sys->device_list,
                                      x)))
            {
575
                cfn_device_id = CFArrayGetValueAtIndex(p_sys->device_list, x);
Thomas Guillem's avatar
Thomas Guillem committed
576 577 578 579 580 581
                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);
582 583 584 585 586

                    ReportDevice(p_aout, i_device_id, NULL);
                }
            }
        }
587
    }
588 589
    if (p_sys->device_list)
        CFRelease(p_sys->device_list);
Thomas Guillem's avatar
Thomas Guillem committed
590 591
    p_sys->device_list = CFArrayCreateCopy(kCFAllocatorDefault,
                                           currentListOfDevices);
592 593
    CFRelease(currentListOfDevices);
    vlc_mutex_unlock(&p_sys->device_list_lock);
594

595
    free(p_devices);
596
}
597

598 599 600
/*
 * Callback when current device is not alive anymore
 */
Thomas Guillem's avatar
Thomas Guillem committed
601 602 603 604
static OSStatus
DeviceAliveListener(AudioObjectID inObjectID,  UInt32 inNumberAddresses,
                    const AudioObjectPropertyAddress inAddresses[],
                    void *inClientData)
605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
{
    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
 */
Thomas Guillem's avatar
Thomas Guillem committed
623 624 625 626
static OSStatus
DefaultDeviceChangedListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
                             const AudioObjectPropertyAddress inAddresses[],
                             void *inClientData)
627 628 629 630 631 632 633 634 635 636 637 638 639 640
{
    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_aout->sys->b_selected_dev_is_default)
        return noErr;

641 642 643 644 645 646
    AudioObjectID defaultDeviceID;
    int ret = AO_GET1PROP(kAudioObjectSystemObject, AudioObjectID,
                          &defaultDeviceID,
                          kAudioHardwarePropertyDefaultOutputDevice,
                          kAudioObjectPropertyScopeOutput);
    if (ret != VLC_SUCCESS)
647
        return -1;
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
648

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

Thomas Guillem's avatar
Thomas Guillem committed
651 652 653 654
    /* 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_aout->sys->b_digital)
    {
655 656
        msg_Dbg(p_aout, "ignore, as digital mode is active");
        return noErr;
657 658
    }

659 660
    vlc_mutex_lock(&p_sys->selected_device_lock);
    /* Also ignore events which announce the same device id */
Thomas Guillem's avatar
Thomas Guillem committed
661 662
    if (defaultDeviceID != p_aout->sys->i_selected_dev)
    {
663 664 665 666 667 668
        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;
669 670
}

671
/*
672
 * Callback when physical formats for device change
673
 */
Thomas Guillem's avatar
Thomas Guillem committed
674 675 676 677
static OSStatus
StreamsChangedListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
                       const AudioObjectPropertyAddress inAddresses[],
                       void *inClientData)
678
{
679 680
    VLC_UNUSED(inNumberAddresses);
    VLC_UNUSED(inAddresses);
681

682 683 684
    audio_output_t *p_aout = (audio_output_t *)inClientData;
    if (!p_aout)
        return -1;
685

686 687 688 689 690
    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");
691
    RebuildDeviceList(p_aout, NULL);
692 693

    vlc_mutex_lock(&p_sys->selected_device_lock);
Thomas Guillem's avatar
Thomas Guillem committed
694 695 696 697
    /* 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)
    {
698 699
        vlc_mutex_unlock(&p_sys->selected_device_lock);
        return 0;
700 701
    }

702 703 704
    /*
     * check if changed stream id belongs to current device
     */
705 706 707 708 709
    size_t i_streams;
    AudioStreamID *p_streams;
    int ret = AO_GETPROP(p_sys->i_selected_dev, AudioStreamID, &i_streams,
                         &p_streams, kAudioDevicePropertyStreams,
                         kAudioObjectPropertyScopeOutput);
Thomas Guillem's avatar
Thomas Guillem committed
710 711
    if (ret != VLC_SUCCESS)
    {
712
        vlc_mutex_unlock(&p_sys->selected_device_lock);
713
        return ret;
714
    }
715
    vlc_mutex_unlock(&p_sys->selected_device_lock);
716

Thomas Guillem's avatar
Thomas Guillem committed
717 718 719 720
    for (size_t i = 0; i < i_streams; i++)
    {
        if (p_streams[i] == inObjectID)
        {
721 722 723 724 725 726
            msg_Dbg(p_aout, "Restart aout as this affects current device");
            aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
            break;
        }
    }
    free(p_streams);
727

728 729
    return noErr;
}
730

731 732 733
/*
 * Callback when device list changed
 */
Thomas Guillem's avatar
Thomas Guillem committed
734 735 736 737
static OSStatus
DevicesListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
                const AudioObjectPropertyAddress inAddresses[],
                void *inClientData)
738 739 740 741
{
    VLC_UNUSED(inObjectID);
    VLC_UNUSED(inNumberAddresses);
    VLC_UNUSED(inAddresses);
742

743 744 745 746
    audio_output_t *p_aout = (audio_output_t *)inClientData;
    if (!p_aout)
        return -1;
    aout_sys_t *p_sys = p_aout->sys;
747

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

751 752
    vlc_mutex_lock(&p_sys->selected_device_lock);
    vlc_mutex_lock(&p_sys->device_list_lock);
Thomas Guillem's avatar
Thomas Guillem committed
753 754 755 756 757
    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))
758 759 760 761
        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);
762

763 764
    return noErr;
}
765

766 767 768
/*
 * StreamListener: check whether the device's physical format change is complete
 */
Thomas Guillem's avatar
Thomas Guillem committed
769 770 771 772
static OSStatus
StreamListener(AudioObjectID inObjectID, UInt32 inNumberAddresses,
               const AudioObjectPropertyAddress inAddresses[],
               void *inClientData)
773 774 775
{
    OSStatus err = noErr;
    struct { vlc_mutex_t lock; vlc_cond_t cond; } * w = inClientData;
776

777
    VLC_UNUSED(inObjectID);
778

Thomas Guillem's avatar
Thomas Guillem committed
779 780 781 782
    for (unsigned int i = 0; i < inNumberAddresses; i++)
    {
        if (inAddresses[i].mSelector == kAudioStreamPropertyPhysicalFormat)
        {
783 784 785 786 787 788
            int canc = vlc_savecancel();
            vlc_mutex_lock(&w->lock);
            vlc_cond_signal(&w->cond);
            vlc_mutex_unlock(&w->lock);
            vlc_restorecancel(canc);
            break;
789
        }
790
    }
791 792
    return err;
}
793

794
/*
Thomas Guillem's avatar
Thomas Guillem committed
795 796
 * AudioStreamChangeFormat: switch stream format based on the provided
 * description
797
 */
Thomas Guillem's avatar
Thomas Guillem committed
798 799 800
static int
AudioStreamChangeFormat(audio_output_t *p_aout, AudioStreamID i_stream_id,
                        AudioStreamBasicDescription change_format)
801 802
{
    int retValue = false;
803

804
    struct { vlc_mutex_t lock; vlc_cond_t cond; } w;
805

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

808 809 810 811
    /* Condition because SetProperty is asynchronious */
    vlc_cond_init(&w.cond);
    vlc_mutex_init(&w.lock);
    vlc_mutex_lock(&w.lock);
812

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

Thomas Guillem's avatar
Thomas Guillem committed
818 819
    if (ret != VLC_SUCCESS)
    {
820 821 822
        retValue = false;
        goto out;
    }
823

824
    /* change the format */
825 826 827
    ret = AO_SETPROP(i_stream_id, sizeof(AudioStreamBasicDescription),
                     &change_format, kAudioStreamPropertyPhysicalFormat,
                     kAudioObjectPropertyScopeGlobal);
Thomas Guillem's avatar
Thomas Guillem committed
828 829
    if (ret != VLC_SUCCESS)
    {
830 831 832
        retValue = false;
        goto out;
    }
833

Thomas Guillem's avatar
Thomas Guillem committed
834 835 836
    /* The AudioStreamSetProperty is not only asynchronious (requiring the
     * locks) it is also not atomic in its behaviour.  Therefore we check 9
     * times before we really give up.
837
     */
Thomas Guillem's avatar
Thomas Guillem committed
838 839 840 841 842 843
    for (int i = 0; i < 9; i++)
    {
        /* Callback is not always invoked. So first check if format is already
         * set. */
        if (i > 0)
        {
844 845 846 847
            mtime_t timeout = mdate() + 500000;
            if (vlc_cond_timedwait(&w.cond, &w.lock, timeout))
                msg_Dbg(p_aout, "reached timeout");
        }
848

849 850 851 852 853 854 855 856
        AudioStreamBasicDescription actual_format;
        int ret = AO_GET1PROP(i_stream_id, AudioStreamBasicDescription,
                              &actual_format,
                              kAudioStreamPropertyPhysicalFormat,
                              kAudioObjectPropertyScopeGlobal);

        if (ret != VLC_SUCCESS)
            continue;
857

Thomas Guillem's avatar
Thomas Guillem committed
858 859
        msg_Dbg(p_aout, STREAM_FORMAT_MSG("actual format in use: ",
                actual_format));
860 861
        if (actual_format.mSampleRate == change_format.mSampleRate &&
            actual_format.mFormatID == change_format.mFormatID &&
Thomas Guillem's avatar
Thomas Guillem committed
862 863
            actual_format.mFramesPerPacket == change_format.mFramesPerPacket)
        {
864 865
            /* The right format is now active */
            retValue = true;
866
            break;
867
        }
868

869
        /* We need to check again */
870
    }
871

872 873
out:
    vlc_mutex_unlock(&w.lock);
874

875
    /* Removing the property listener */
876 877 878 879
    ret = AO_UPDATELISTENER(i_stream_id, false, StreamListener, &w,
                            kAudioStreamPropertyPhysicalFormat,
                            kAudioObjectPropertyScopeGlobal);
    if (ret != VLC_SUCCESS)
880
        retValue = false;
881

882 883
    vlc_mutex_destroy(&w.lock);
    vlc_cond_destroy(&w.cond);
884

885 886
    return retValue;
}
887

888 889
#pragma mark -
#pragma mark core interaction
890

Thomas Guillem's avatar
Thomas Guillem committed
891 892
static int
SwitchAudioDevice(audio_output_t *p_aout, const char *name)
893 894
{
    struct aout_sys_t *p_sys = p_aout->sys;
895

896 897 898 899
    if (name)
        p_sys->i_new_selected_dev = atoi(name);
    else
        p_sys->i_new_selected_dev = 0;
900

901
    p_sys->i_new_selected_dev = p_sys->i_new_selected_dev;
902

903 904
    aout_DeviceReport(p_aout, name);
    aout_RestartRequest(p_aout, AOUT_RESTART_OUTPUT);
905

906
    return 0;
907 908
}

Thomas Guillem's avatar
Thomas Guillem committed
909 910
static int
VolumeSet(audio_output_t * p_aout, float volume)
911
{
912 913
    struct aout_sys_t *p_sys = p_aout->sys;
    OSStatus ostatus = 0;
914

Thomas Guillem's avatar
Thomas Guillem committed
915
    if (p_sys->b_digital)
916
        return VLC_EGENERIC;
917

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

921
    /* Set volume for output unit */
Thomas Guillem's avatar
Thomas Guillem committed
922 923
    if (!p_sys->b_mute)
    {
924 925 926 927 928 929
        ostatus = AudioUnitSetParameter(p_sys->au_unit,
                                        kHALOutputParam_Volume,
                                        kAudioUnitScope_Global,
                                        0,
                                        volume * volume * volume,
                                        0);
930 931
    }

932
    if (var_InheritBool(p_aout, "volume-save"))
Thomas Guillem's avatar
Thomas Guillem committed
933 934
        config_PutInt(p_aout, "auhal-volume",
                      lroundf(volume * AOUT_VOLUME_DEFAULT));
935

936 937
    return ostatus;
}
938

Thomas Guillem's avatar
Thomas Guillem committed
939 940
static int
MuteSet(audio_output_t * p_aout, bool mute)
941
{
Thomas Guillem's avatar
Thomas Guillem committed
942
    struct aout_sys_t *p_sys = p_aout->sys;
Derk-Jan Hartman's avatar
Derk-Jan Hartman committed
943

944 945
    if(p_sys->b_digital)
        return VLC_EGENERIC;
946

947 948
    p_sys->b_mute = mute;
    aout_MuteReport(p_aout, mute);
949

950 951 952
    float volume = .0;
    if (!mute)
        volume = p_sys->f_volume;
953

Thomas Guillem's avatar
Thomas Guillem committed
954 955 956 957
    OSStatus err =
        AudioUnitSetParameter(p_sys->au_unit, kHALOutputParam_Volume,
                              kAudioUnitScope_Global, 0,
                              volume * volume * volume, 0);
958

Thomas Guillem's avatar
Thomas Guillem committed
959
    return err == noErr ? VLC_SUCCESS : VLC_EGENERIC;
960
}
961

962 963
#pragma mark -
#pragma mark actual playback
964

965 966 967
/*
 * RenderCallbackSPDIF: callback for SPDIF audio output
 */
Thomas Guillem's avatar
Thomas Guillem committed
968 969
static OSStatus
RenderCallbackSPDIF(AudioDeviceID inDevice, const AudioTimeStamp * inNow,
970
                    const AudioBufferList * inInputData,
Thomas Guillem's avatar
Thomas Guillem committed
971 972
                    const AudioTimeStamp * inInputTime,
                    AudioBufferList * outOutputData,
973
                    const AudioTimeStamp * inOutputTime, void *p_data)
974
{
975 976 977 978 979
    VLC_UNUSED(inNow);
    VLC_UNUSED(inDevice);
    VLC_UNUSED(inInputData);
    VLC_UNUSED(inInputTime);
    VLC_UNUSED(inOutputTime);
980

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

986
    ca_Render(p_aout, p_output, i_size);
987

988 989
    return noErr;
}
990

991 992
#pragma mark -
#pragma mark initialization
993

994 995 996
/*
 * StartAnalog: open and setup a HAL AudioUnit to do PCM audio output
 */
Thomas Guillem's avatar
Thomas Guillem committed
997
static int
998 999
StartAnalog(audio_output_t *p_aout, audio_sample_format_t *fmt,
            mtime_t i_latency_us)
1000 1001 1002
{
    struct aout_sys_t           *p_sys = p_aout->sys;
    OSStatus                    err = noErr;
1003 1004
    UInt32                      i_param_size;
    AudioChannelLayout          *layout = NULL;
1005

1006 1007 1008
    if (aout_FormatNbChannels(fmt) == 0)
        return VLC_EGENERIC;

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

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

1015 1016
    /* Set the device we will use for this output unit */
    err = AudioUnitSetProperty(p_sys->au_unit,
Thomas Guillem's avatar
Thomas Guillem committed
1017 1018 1019 1020 1021 1022
                               kAudioOutputUnitProperty_CurrentDevice,
                               kAudioUnitScope_Global, 0,
                               &p_sys->i_selected_dev, sizeof(AudioObjectID));

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

Thomas Guillem's avatar
Thomas Guillem committed
1027 1028
    /* Get the channel layout of the device side of the unit (vlc -> unit ->
     * device) */
1029 1030
    err = AudioUnitGetPropertyInfo(p_sys->au_unit,
                                   kAudioDevicePropertyPreferredChannelLayout,
Thomas Guillem's avatar
Thomas Guillem committed
1031
                                   kAudioUnitScope_Output, 0, &i_param_size,
1032
                                   NULL);
Thomas Guillem's avatar
Thomas Guillem committed
1033 1034
    if (err == noErr)
    {
1035
        layout = (AudioChannelLayout *)malloc(i_param_size);
1036 1037
        if (layout == NULL)
            goto error;
1038

1039 1040 1041 1042 1043 1044
        OSStatus err =
            AudioUnitGetProperty(p_sys->au_unit,
                                 kAudioDevicePropertyPreferredChannelLayout,
                                 kAudioUnitScope_Output, 0, layout,
                                 &i_param_size);
        if (err != noErr)
1045
            goto error;
Thomas Guillem's avatar
Thomas Guillem committed
1046 1047
    }
    else
1048 1049
        ca_LogWarn("device driver does not support "
                   "kAudioDevicePropertyPreferredChannelLayout - using stereo");
1050

1051
    /* Do the last VLC aout setups */
1052 1053 1054
    bool warn_configuration;
    int ret = au_Initialize(p_aout, p_sys->au_unit, fmt, layout, i_latency_us,
                            &warn_configuration);
1055
    if (ret != VLC_SUCCESS)
1056
        goto error;
1057

1058 1059 1060
    err = AudioOutputUnitStart(p_sys->au_unit);
    if (err != noErr)
    {
1061
        ca_LogErr("AudioUnitStart failed");
1062
        au_Uninitialize(p_aout, p_sys->au_unit);
1063 1064
        goto error;
    }
1065

1066 1067 1068
    /* Set volume for output unit */
    VolumeSet(p_aout, p_sys->f_volume);
    MuteSet(p_aout, p_sys->b_mute);
1069

1070
    free(layout);
1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083

    if (warn_configuration)
    {
        msg_Err(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."));
    }

1084
    return VLC_SUCCESS;
1085 1086
error:
    AudioComponentInstanceDispose(p_sys->au_unit);
1087
    free(layout);
1088
    return VLC_EGENERIC;
1089 1090
}

1091 1092 1093
/*
 * StartSPDIF: Setup an encoded digital stream (SPDIF) output
 */
Thomas Guillem's avatar
Thomas Guillem committed
1094
static int
1095 1096
StartSPDIF(audio_output_t * p_aout, audio_sample_format_t *fmt,
           mtime_t i_latency_us)
1097
{
1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127
    struct 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;
    }

1128 1129
    AudioStreamBasicDescription desired_stream_format;
    memset(&desired_stream_format, 0, sizeof(desired_stream_format));
1130

1131 1132
    /* Start doing the SPDIF setup proces */
    p_sys->b_digital = true;
1133

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

1137
    /*
Thomas Guillem's avatar
Thomas Guillem committed
1138 1139 1140
     * 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.
1141 1142
     */
    p_sys->b_ignore_streams_changed_callback = true;
1143 1144 1145
    ret = AO_SETPROP(p_sys->i_selected_dev, sizeof(p_sys->i_hog_pid),
                     &p_sys->i_hog_pid, kAudioDevicePropertyHogMode,
                     kAudioObjectPropertyScopeOutput);