Merge branch 'cstrahan/macos-follow-system-default' into 'master'

macosx: track system default output/input device

See merge request pulseaudio/pulseaudio!867
This commit is contained in:
Charles Strahan 2026-04-27 18:54:05 +00:00
commit 6238acda9b

View file

@ -24,27 +24,38 @@
#include <pulse/xmalloc.h>
#include <pulsecore/module.h>
#include <pulsecore/core.h>
#include <pulsecore/core-util.h>
#include <pulsecore/modargs.h>
#include <pulsecore/log.h>
#include <pulsecore/llist.h>
#include <pulsecore/sink.h>
#include <pulsecore/source.h>
#include <CoreAudio/CoreAudio.h>
#define DEVICE_MODULE_NAME "module-coreaudio-device"
/* Event tags multiplexed over the self-pipe from CoreAudio property
* listeners to the main thread. */
#define CA_EVENT_DEVICE_LIST '\x01'
#define CA_EVENT_DEFAULT_OUTPUT '\x02'
#define CA_EVENT_DEFAULT_INPUT '\x03'
PA_MODULE_AUTHOR("Daniel Mack");
PA_MODULE_DESCRIPTION("CoreAudio device detection");
PA_MODULE_VERSION(PACKAGE_VERSION);
PA_MODULE_LOAD_ONCE(true);
PA_MODULE_USAGE("ioproc_frames=<passed on to module-coreaudio-device> "
"record=<enable source?> "
"playback=<enable sink?> ");
"playback=<enable sink?> "
"follow_system_default=<track macOS default output/input device?> ");
static const char* const valid_modargs[] = {
"ioproc_frames",
"record",
"playback",
"follow_system_default",
NULL
};
@ -62,9 +73,16 @@ struct userdata {
unsigned int ioproc_frames;
bool record;
bool playback;
bool follow_system_default;
pa_hook_slot *sink_put_slot, *source_put_slot;
PA_LLIST_HEAD(ca_device, devices);
};
static pa_sink *ca_find_sink_for_object_id(pa_module *m, AudioObjectID id);
static pa_source *ca_find_source_for_object_id(pa_module *m, AudioObjectID id);
static void ca_apply_system_default_sink(pa_module *m);
static void ca_apply_system_default_source(pa_module *m);
static int ca_device_added(struct pa_module *m, AudioObjectID id) {
AudioObjectPropertyAddress property_address;
OSStatus err;
@ -186,28 +204,208 @@ scan_removed:
return 0;
}
static pa_sink *ca_find_sink_for_object_id(pa_module *m, AudioObjectID id) {
struct userdata *u;
struct ca_device *dev;
pa_sink *sink;
uint32_t idx;
pa_assert(m);
pa_assert_se(u = m->userdata);
PA_LLIST_FOREACH(dev, u->devices) {
if (dev->id != id)
continue;
PA_IDXSET_FOREACH(sink, m->core->sinks, idx) {
if (sink->module && sink->module->index == dev->module_index)
return sink;
}
}
return NULL;
}
static pa_source *ca_find_source_for_object_id(pa_module *m, AudioObjectID id) {
struct userdata *u;
struct ca_device *dev;
pa_source *source;
uint32_t idx;
pa_assert(m);
pa_assert_se(u = m->userdata);
PA_LLIST_FOREACH(dev, u->devices) {
if (dev->id != id)
continue;
PA_IDXSET_FOREACH(source, m->core->sources, idx) {
/* Skip sinks' monitor sources -- those belong to the sink's module
* but are inputs of a different nature. Prefer real hardware sources. */
if (source->monitor_of)
continue;
if (source->module && source->module->index == dev->module_index)
return source;
}
}
return NULL;
}
static AudioDeviceID ca_get_system_default(AudioObjectPropertySelector selector) {
AudioObjectPropertyAddress property_address;
AudioDeviceID id = kAudioObjectUnknown;
UInt32 size = sizeof(id);
OSStatus err;
property_address.mSelector = selector;
property_address.mScope = kAudioObjectPropertyScopeGlobal;
property_address.mElement = kAudioObjectPropertyElementMaster;
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &property_address,
0, NULL, &size, &id);
if (err) {
pa_log_debug("Unable to read CoreAudio default device (selector %u): %d",
(unsigned int) selector, (int) err);
return kAudioObjectUnknown;
}
return id;
}
static void ca_apply_system_default_sink(pa_module *m) {
struct userdata *u;
AudioDeviceID id;
pa_sink *sink;
pa_assert(m);
pa_assert_se(u = m->userdata);
if (!u->follow_system_default || !u->playback)
return;
id = ca_get_system_default(kAudioHardwarePropertyDefaultOutputDevice);
if (id == kAudioObjectUnknown)
return;
sink = ca_find_sink_for_object_id(m, id);
if (!sink) {
/* The device module may not have finished creating its sink yet; the
* sink_put hook will pick this up when it does. */
pa_log_debug("CoreAudio default output device %u has no matching sink yet",
(unsigned int) id);
return;
}
pa_log_info("Following macOS default output: setting policy default sink to '%s'", sink->name);
pa_core_set_policy_default_sink(m->core, sink->name);
}
static void ca_apply_system_default_source(pa_module *m) {
struct userdata *u;
AudioDeviceID id;
pa_source *source;
pa_assert(m);
pa_assert_se(u = m->userdata);
if (!u->follow_system_default || !u->record)
return;
id = ca_get_system_default(kAudioHardwarePropertyDefaultInputDevice);
if (id == kAudioObjectUnknown)
return;
source = ca_find_source_for_object_id(m, id);
if (!source) {
pa_log_debug("CoreAudio default input device %u has no matching source yet",
(unsigned int) id);
return;
}
pa_log_info("Following macOS default input: setting policy default source to '%s'", source->name);
pa_core_set_policy_default_source(m->core, source->name);
}
static pa_hook_result_t sink_put_hook_cb(pa_core *c, pa_sink *sink, pa_module *m) {
pa_assert(c);
pa_assert(sink);
pa_assert(m);
/* A new sink appeared; it may belong to the device that CoreAudio
* considers the current default. Re-apply so startup and hot-plug
* races end up with the correct default. */
ca_apply_system_default_sink(m);
return PA_HOOK_OK;
}
static pa_hook_result_t source_put_hook_cb(pa_core *c, pa_source *source, pa_module *m) {
pa_assert(c);
pa_assert(source);
pa_assert(m);
ca_apply_system_default_source(m);
return PA_HOOK_OK;
}
static OSStatus property_listener_proc(AudioObjectID objectID, UInt32 numberAddresses,
const AudioObjectPropertyAddress inAddresses[],
void *clientData) {
struct userdata *u = clientData;
char dummy = 1;
UInt32 i;
pa_assert(u);
/* dispatch module load/unload operations in main thread */
write(u->detect_fds[1], &dummy, 1);
/* Translate each triggered CoreAudio property into a tagged wake-up for the
* main thread. CoreAudio may batch several selectors into one callback. */
for (i = 0; i < numberAddresses; i++) {
char tag;
switch (inAddresses[i].mSelector) {
case kAudioHardwarePropertyDevices:
tag = CA_EVENT_DEVICE_LIST;
break;
case kAudioHardwarePropertyDefaultOutputDevice:
tag = CA_EVENT_DEFAULT_OUTPUT;
break;
case kAudioHardwarePropertyDefaultInputDevice:
tag = CA_EVENT_DEFAULT_INPUT;
break;
default:
continue;
}
write(u->detect_fds[1], &tag, 1);
}
return 0;
}
static void detect_handle(pa_mainloop_api *a, pa_io_event *e, int fd, pa_io_event_flags_t events, void *userdata) {
pa_module *m = userdata;
char dummy;
char tag;
ssize_t n;
pa_assert(m);
read(fd, &dummy, 1);
ca_update_device_list(m);
n = read(fd, &tag, 1);
if (n != 1)
return;
switch (tag) {
case CA_EVENT_DEVICE_LIST:
ca_update_device_list(m);
break;
case CA_EVENT_DEFAULT_OUTPUT:
ca_apply_system_default_sink(m);
break;
case CA_EVENT_DEFAULT_INPUT:
ca_apply_system_default_source(m);
break;
default:
pa_log_debug("Unknown coreaudio detect event tag: %d", (int) tag);
break;
}
}
int pa__init(pa_module *m) {
@ -218,6 +416,7 @@ int pa__init(pa_module *m) {
pa_assert(m);
pa_assert(m->core);
u->detect_fds[0] = u->detect_fds[1] = -1;
m->userdata = u;
if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
@ -231,12 +430,18 @@ int pa__init(pa_module *m) {
* buffer.
*/
u->playback = u->record = true;
u->follow_system_default = true;
if (pa_modargs_get_value_boolean(ma, "record", &u->record) < 0 || pa_modargs_get_value_boolean(ma, "playback", &u->playback) < 0) {
pa_log("record= and playback= expect boolean argument.");
goto fail;
}
if (pa_modargs_get_value_boolean(ma, "follow_system_default", &u->follow_system_default) < 0) {
pa_log("follow_system_default= expects a boolean argument.");
goto fail;
}
if (!u->playback && !u->record) {
pa_log("neither playback nor record enabled for device.");
goto fail;
@ -244,25 +449,55 @@ int pa__init(pa_module *m) {
pa_modargs_get_value_u32(ma, "ioproc_frames", &u->ioproc_frames);
property_address.mSelector = kAudioHardwarePropertyDevices;
property_address.mScope = kAudioObjectPropertyScopeGlobal;
property_address.mElement = kAudioObjectPropertyElementMaster;
property_address.mSelector = kAudioHardwarePropertyDevices;
if (AudioObjectAddPropertyListener(kAudioObjectSystemObject, &property_address, property_listener_proc, u)) {
pa_log("AudioObjectAddPropertyListener() failed.");
pa_log("AudioObjectAddPropertyListener() failed for device list.");
goto fail;
}
if (u->follow_system_default) {
if (u->playback) {
property_address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
if (AudioObjectAddPropertyListener(kAudioObjectSystemObject, &property_address, property_listener_proc, u))
pa_log_warn("AudioObjectAddPropertyListener() failed for default output; macOS default tracking disabled for output.");
}
if (u->record) {
property_address.mSelector = kAudioHardwarePropertyDefaultInputDevice;
if (AudioObjectAddPropertyListener(kAudioObjectSystemObject, &property_address, property_listener_proc, u))
pa_log_warn("AudioObjectAddPropertyListener() failed for default input; macOS default tracking disabled for input.");
}
}
if (ca_update_device_list(m))
goto fail;
pa_assert_se(pipe(u->detect_fds) == 0);
pa_assert_se(u->detect_io = m->core->mainloop->io_new(m->core->mainloop, u->detect_fds[0], PA_IO_EVENT_INPUT, detect_handle, m));
if (u->follow_system_default) {
/* React to sinks/sources becoming live after a default-change
* notification arrived too early (hot-plug race). */
u->sink_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SINK_PUT],
PA_HOOK_LATE, (pa_hook_cb_t) sink_put_hook_cb, m);
u->source_put_slot = pa_hook_connect(&m->core->hooks[PA_CORE_HOOK_SOURCE_PUT],
PA_HOOK_LATE, (pa_hook_cb_t) source_put_hook_cb, m);
/* Sync the initial default with whatever CoreAudio currently reports. */
ca_apply_system_default_sink(m);
ca_apply_system_default_source(m);
}
pa_modargs_free(ma);
return 0;
fail:
pa_xfree(u);
if (ma)
pa_modargs_free(ma);
pa__done(m);
return -1;
}
@ -272,16 +507,29 @@ void pa__done(pa_module *m) {
AudioObjectPropertyAddress property_address;
pa_assert(m);
pa_assert_se(u = m->userdata);
if (!(u = m->userdata))
return;
dev = u->devices;
property_address.mSelector = kAudioHardwarePropertyDevices;
property_address.mScope = kAudioObjectPropertyScopeGlobal;
property_address.mElement = kAudioObjectPropertyElementMaster;
property_address.mSelector = kAudioHardwarePropertyDevices;
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &property_address, property_listener_proc, u);
/* These are no-ops if the listeners were never installed. */
property_address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &property_address, property_listener_proc, u);
property_address.mSelector = kAudioHardwarePropertyDefaultInputDevice;
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &property_address, property_listener_proc, u);
if (u->sink_put_slot)
pa_hook_slot_free(u->sink_put_slot);
if (u->source_put_slot)
pa_hook_slot_free(u->source_put_slot);
while (dev) {
struct ca_device *next = dev->next;
@ -301,4 +549,5 @@ void pa__done(pa_module *m) {
m->core->mainloop->io_free(u->detect_io);
pa_xfree(u);
m->userdata = NULL;
}