diff --git a/plugins/mac-capture/CMakeLists.txt b/plugins/mac-capture/CMakeLists.txt index 31d896a0130ae5..3d1ec8901cfb8f 100644 --- a/plugins/mac-capture/CMakeLists.txt +++ b/plugins/mac-capture/CMakeLists.txt @@ -8,7 +8,7 @@ target_sources( PRIVATE audio-device-enum.c audio-device-enum.h - mac-audio.c + mac-audio.m mac-display-capture.m mac-sck-audio-capture.m mac-sck-common.h @@ -31,6 +31,7 @@ target_link_libraries( "$" "$" "$" + "$" "$" ) diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index 40c62a4e3604fc..399c92c3782e05 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -37,3 +37,5 @@ SCK.AudioUnavailable="Audio capture requires macOS 13 or newer." SCK.CaptureTypeUnavailable="Selected capture type requires macOS 13 or newer." SCK.Method="Method" SCK.Restart="Restart capture" +Warning.Effect.Audio.VoiceIsolation="The macOS system effect 'Voice Isolation' is active on the selected device" +Warning.Effect.Audio.WideSpectrum="The macOS system effect 'Wide Spectrum' is active on the selected device" diff --git a/plugins/mac-capture/mac-audio.c b/plugins/mac-capture/mac-audio.c deleted file mode 100644 index 6840166511e5e1..00000000000000 --- a/plugins/mac-capture/mac-audio.c +++ /dev/null @@ -1,1057 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include "audio-device-enum.h" - -#define PROPERTY_DEFAULT_DEVICE kAudioHardwarePropertyDefaultInputDevice -#define PROPERTY_FORMATS kAudioStreamPropertyAvailablePhysicalFormats - -#define SCOPE_OUTPUT kAudioUnitScope_Output -#define SCOPE_INPUT kAudioUnitScope_Input -#define SCOPE_GLOBAL kAudioUnitScope_Global - -#define BUS_OUTPUT 0 -#define BUS_INPUT 1 - -#define set_property AudioUnitSetProperty -#define get_property AudioUnitGetProperty - -#define TEXT_AUDIO_INPUT obs_module_text("CoreAudio.InputCapture"); -#define TEXT_AUDIO_OUTPUT obs_module_text("CoreAudio.OutputCapture"); -#define TEXT_DEVICE obs_module_text("CoreAudio.Device") -#define TEXT_DEVICE_DEFAULT obs_module_text("CoreAudio.Device.Default") - -struct coreaudio_data { - char *device_name; - char *device_uid; - AudioUnit unit; - AudioDeviceID device_id; - AudioBufferList *buf_list; - bool au_initialized; - bool active; - bool default_device; - bool input; - bool no_devices; - - uint32_t available_channels; - char **channel_names; - int32_t *channel_map; - - uint32_t sample_rate; - enum audio_format format; - enum speaker_layout speakers; - bool enable_downmix; - - pthread_t reconnect_thread; - os_event_t *exit_event; - volatile bool reconnecting; - unsigned long retry_time; - - obs_source_t *source; -}; - -static bool get_default_output_device(struct coreaudio_data *ca) -{ - struct device_list list; - - memset(&list, 0, sizeof(struct device_list)); - coreaudio_enum_devices(&list, false); - - if (!list.items.num) - return false; - - bfree(ca->device_uid); - ca->device_uid = bstrdup(list.items.array[0].value.array); - - device_list_free(&list); - return true; -} - -static bool find_device_id_by_uid(struct coreaudio_data *ca) -{ - UInt32 size = sizeof(AudioDeviceID); - CFStringRef cf_uid = NULL; - CFStringRef qual = NULL; - UInt32 qual_size = 0; - OSStatus stat; - bool success; - - AudioObjectPropertyAddress addr = {.mScope = kAudioObjectPropertyScopeGlobal, - .mElement = kAudioObjectPropertyElementMain}; - - if (!ca->device_uid) - ca->device_uid = bstrdup("default"); - - ca->default_device = false; - ca->no_devices = false; - - /* have to do this because mac output devices don't actually exist */ - if (astrcmpi(ca->device_uid, "default") == 0) { - if (ca->input) { - ca->default_device = true; - } else { - if (!get_default_output_device(ca)) { - ca->no_devices = true; - return false; - } - } - } - - cf_uid = CFStringCreateWithCString(NULL, ca->device_uid, kCFStringEncodingUTF8); - - if (ca->default_device) { - addr.mSelector = kAudioHardwarePropertyDefaultInputDevice; - stat = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, qual_size, &qual, &size, - &ca->device_id); - success = (stat == noErr); - } else { - success = coreaudio_get_device_id(cf_uid, &ca->device_id); - } - - if (cf_uid) - CFRelease(cf_uid); - - return success; -} - -static inline void ca_warn(struct coreaudio_data *ca, const char *func, const char *format, ...) -{ - va_list args; - struct dstr str = {0}; - - va_start(args, format); - - dstr_printf(&str, "[%s]:[device '%s'] ", func, ca->device_name); - dstr_vcatf(&str, format, args); - blog(LOG_WARNING, "%s", str.array); - dstr_free(&str); - - va_end(args); -} - -static inline bool ca_success(OSStatus stat, struct coreaudio_data *ca, const char *func, const char *action) -{ - if (stat != noErr) { - blog(LOG_WARNING, "[%s]:[device '%s'] %s failed: %d", func, ca->device_name, action, (int)stat); - return false; - } - - return true; -} - -enum coreaudio_io_type { - IO_TYPE_INPUT, - IO_TYPE_OUTPUT, -}; - -static inline bool enable_io(struct coreaudio_data *ca, enum coreaudio_io_type type, bool enable) -{ - UInt32 enable_int = enable; - return set_property(ca->unit, kAudioOutputUnitProperty_EnableIO, - (type == IO_TYPE_INPUT) ? SCOPE_INPUT : SCOPE_OUTPUT, - (type == IO_TYPE_INPUT) ? BUS_INPUT : BUS_OUTPUT, &enable_int, sizeof(enable_int)); -} - -static inline enum speaker_layout convert_ca_speaker_layout(UInt32 channels) -{ - switch (channels) { - case 1: - return SPEAKERS_MONO; - case 2: - return SPEAKERS_STEREO; - case 3: - return SPEAKERS_2POINT1; - case 4: - return SPEAKERS_4POINT0; - case 5: - return SPEAKERS_4POINT1; - case 6: - return SPEAKERS_5POINT1; - case 8: - return SPEAKERS_7POINT1; - } - return SPEAKERS_UNKNOWN; -} - -static inline enum audio_format convert_ca_format(UInt32 format_flags, UInt32 bits) -{ - bool planar = (format_flags & kAudioFormatFlagIsNonInterleaved) != 0; - - if (format_flags & kAudioFormatFlagIsFloat) - return planar ? AUDIO_FORMAT_FLOAT_PLANAR : AUDIO_FORMAT_FLOAT; - - if (!(format_flags & kAudioFormatFlagIsSignedInteger) && bits == 8) - return planar ? AUDIO_FORMAT_U8BIT_PLANAR : AUDIO_FORMAT_U8BIT; - - /* not float? not signed int? no clue, fail */ - if ((format_flags & kAudioFormatFlagIsSignedInteger) == 0) - return AUDIO_FORMAT_UNKNOWN; - - if (bits == 16) - return planar ? AUDIO_FORMAT_16BIT_PLANAR : AUDIO_FORMAT_16BIT; - else if (bits == 32) - return planar ? AUDIO_FORMAT_32BIT_PLANAR : AUDIO_FORMAT_32BIT; - - return AUDIO_FORMAT_UNKNOWN; -} - -static char *sanitize_device_name(char *name) -{ - const size_t max_len = 64; - size_t len = strlen(name); - char buf[64]; - size_t out_idx = 0; - - for (size_t i = len > max_len ? len - max_len : 0; i < len; i++) { - char c = name[i]; - if (isalnum(c)) { - buf[out_idx++] = name[i]; - } - if (c == '-' || c == ' ' || c == '_' || c == ':') { - buf[out_idx++] = '_'; - } - } - return bstrdup_n(buf, out_idx); -} - -static char **coreaudio_get_channel_names(struct coreaudio_data *ca) -{ - char **channel_names = bzalloc(sizeof(char *) * ca->available_channels); - - for (uint32_t i = 0; i < ca->available_channels; i++) { - CFStringRef cf_chan_name = NULL; - UInt32 dataSize = sizeof(cf_chan_name); - AudioObjectPropertyAddress pa; - pa.mSelector = kAudioObjectPropertyElementName; - pa.mScope = kAudioDevicePropertyScopeInput; - pa.mElement = i + 1; - OSStatus stat = AudioObjectGetPropertyData(ca->device_id, &pa, 0, NULL, &dataSize, &cf_chan_name); - - struct dstr name; - dstr_init(&name); - if (ca_success(stat, ca, "coreaudio_init_format", "get channel names") && - CFStringGetLength(cf_chan_name)) { - - char *channelName = cfstr_copy_cstr(cf_chan_name, kCFStringEncodingUTF8); - - dstr_printf(&name, "%s", channelName); - - if (channelName) { - bfree(channelName); - } - } else { - dstr_printf(&name, "%s %d", obs_module_text("CoreAudio.Channel.Device"), i + 1); - } - channel_names[i] = bstrdup_n(name.array, name.len); - dstr_free(&name); - - if (cf_chan_name) { - CFRelease(cf_chan_name); - } - } - return channel_names; -} - -static bool coreaudio_init_format(struct coreaudio_data *ca) -{ - AudioStreamBasicDescription desc; - AudioStreamBasicDescription inputDescription; - OSStatus stat; - UInt32 size; - struct obs_audio_info aoi; - if (!obs_get_audio_info(&aoi)) { - blog(LOG_WARNING, "No active audio"); - return false; - } - ca->speakers = aoi.speakers; - uint32_t channels = get_audio_channels(ca->speakers); - - size = sizeof(inputDescription); - stat = get_property(ca->unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &inputDescription, - &size); - - if (!ca_success(stat, ca, "coreaudio_init_format", "get input device format")) - return false; - - stat = get_property(ca->unit, kAudioUnitProperty_StreamFormat, SCOPE_OUTPUT, BUS_INPUT, &desc, &size); - if (!ca_success(stat, ca, "coreaudio_init_format", "get input format")) - return false; - - ca->available_channels = inputDescription.mChannelsPerFrame; - if (ca->available_channels > MAX_DEVICE_INPUT_CHANNELS) { - ca->available_channels = MAX_DEVICE_INPUT_CHANNELS; - } - - ca->channel_names = coreaudio_get_channel_names(ca); - - if (ca->enable_downmix) { - blog(LOG_INFO, "Downmix enabled: %d to %d channels.", ca->available_channels, channels); - desc.mChannelsPerFrame = ca->available_channels; - } else { - // Mute any channels mapped in config that we don't really have - char *sep = ""; - struct dstr cm_str; - dstr_init(&cm_str); - for (size_t i = 0; i < channels; i++) { - dstr_cat(&cm_str, sep); - if (ca->channel_map[i] >= (int32_t)ca->available_channels) { - ca->channel_map[i] = -1; - } - dstr_catf(&cm_str, "%d", ca->channel_map[i]); - sep = ","; - } - blog(LOG_INFO, "Channel map enabled: [%s] (%d channels available)", cm_str.array, - ca->available_channels); - dstr_free(&cm_str); - - stat = set_property(ca->unit, kAudioOutputUnitProperty_ChannelMap, SCOPE_OUTPUT, BUS_INPUT, - ca->channel_map, sizeof(SInt32) * channels); - if (!ca_success(stat, ca, "coreaudio_init_format", "set channel map")) { - return false; - } - - desc.mChannelsPerFrame = channels; - } - - desc.mSampleRate = inputDescription.mSampleRate; - - stat = set_property(ca->unit, kAudioUnitProperty_StreamFormat, SCOPE_OUTPUT, BUS_INPUT, &desc, size); - if (!ca_success(stat, ca, "coreaudio_init_format", "set output format")) - return false; - - if (desc.mFormatID != kAudioFormatLinearPCM) { - ca_warn(ca, "coreaudio_init_format", "format is not PCM"); - return false; - } - - ca->format = convert_ca_format(desc.mFormatFlags, desc.mBitsPerChannel); - if (ca->format == AUDIO_FORMAT_UNKNOWN) { - ca_warn(ca, "coreaudio_init_format", - "unknown format flags: " - "%u, bits: %u", - (unsigned int)desc.mFormatFlags, (unsigned int)desc.mBitsPerChannel); - return false; - } - - ca->sample_rate = (uint32_t)desc.mSampleRate; - - return true; -} - -static bool coreaudio_init_buffer(struct coreaudio_data *ca) -{ - UInt32 bufferSizeFrames; - UInt32 bufferSizeBytes; - UInt32 propertySize; - OSStatus err = noErr; - - propertySize = sizeof(bufferSizeFrames); - err = AudioUnitGetProperty(ca->unit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Global, 0, - &bufferSizeFrames, &propertySize); - - if (!ca_success(err, ca, "coreaudio_init_buffer", "get buffer frame size")) { - return false; - } - - bufferSizeBytes = bufferSizeFrames * sizeof(Float32); - - AudioStreamBasicDescription streamDescription; - propertySize = sizeof(streamDescription); - err = AudioUnitGetProperty(ca->unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, - &streamDescription, &propertySize); - - if (!ca_success(err, ca, "coreaudio_init_buffer", "get stream format")) { - return false; - } - - if (!ca->enable_downmix) { - streamDescription.mChannelsPerFrame = get_audio_channels(ca->speakers); - } - - Float64 rate = 0.0; - propertySize = sizeof(Float64); - AudioObjectPropertyAddress propertyAddress = {kAudioDevicePropertyNominalSampleRate, - kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; - - err = AudioObjectGetPropertyData(ca->device_id, &propertyAddress, 0, NULL, &propertySize, &rate); - - if (!ca_success(err, ca, "coreaudio_init_buffer", "get input sample rate")) { - return false; - } - - streamDescription.mSampleRate = rate; - - int bufferPropertySize = - offsetof(AudioBufferList, mBuffers[0]) + (sizeof(AudioBuffer) * streamDescription.mChannelsPerFrame); - - AudioBufferList *inputBuffer = (AudioBufferList *)bmalloc(bufferPropertySize); - inputBuffer->mNumberBuffers = streamDescription.mChannelsPerFrame; - - for (UInt32 i = 0; i < inputBuffer->mNumberBuffers; i++) { - inputBuffer->mBuffers[i].mNumberChannels = 1; - inputBuffer->mBuffers[i].mDataByteSize = bufferSizeBytes; - inputBuffer->mBuffers[i].mData = bmalloc(bufferSizeBytes); - } - - ca->buf_list = inputBuffer; - return true; -} - -static void buf_list_free(AudioBufferList *buf_list) -{ - if (buf_list) { - for (UInt32 i = 0; i < buf_list->mNumberBuffers; i++) - bfree(buf_list->mBuffers[i].mData); - - bfree(buf_list); - } -} - -static OSStatus input_callback(void *data, AudioUnitRenderActionFlags *action_flags, const AudioTimeStamp *ts_data, - UInt32 bus_num, UInt32 frames, AudioBufferList *ignored_buffers) -{ - struct coreaudio_data *ca = data; - OSStatus stat; - struct obs_source_audio audio; - - stat = AudioUnitRender(ca->unit, action_flags, ts_data, bus_num, frames, ca->buf_list); - if (!ca_success(stat, ca, "input_callback", "audio retrieval")) - return noErr; - - for (UInt32 i = 0; i < ca->buf_list->mNumberBuffers; i++) { - if (i < MAX_AUDIO_CHANNELS) { - audio.data[i] = ca->buf_list->mBuffers[i].mData; - } - } - - audio.frames = frames; - audio.speakers = (ca->buf_list->mNumberBuffers > MAX_AUDIO_CHANNELS) ? MAX_AUDIO_CHANNELS - : ca->buf_list->mNumberBuffers; - audio.format = ca->format; - audio.samples_per_sec = ca->sample_rate; - static double factor = 0.; - static mach_timebase_info_data_t info = {0, 0}; - if (info.numer == 0 && info.denom == 0) { - mach_timebase_info(&info); - factor = ((double)info.numer) / info.denom; - } - if (info.numer != info.denom) - audio.timestamp = (uint64_t)(factor * ts_data->mHostTime); - else - audio.timestamp = ts_data->mHostTime; - - obs_source_output_audio(ca->source, &audio); - - UNUSED_PARAMETER(ignored_buffers); - return noErr; -} - -static void coreaudio_stop(struct coreaudio_data *ca); -static bool coreaudio_init(struct coreaudio_data *ca); -static void coreaudio_uninit(struct coreaudio_data *ca); - -static void *reconnect_thread(void *param) -{ - struct coreaudio_data *ca = param; - - ca->reconnecting = true; - - while (os_event_timedwait(ca->exit_event, ca->retry_time) == ETIMEDOUT) { - if (coreaudio_init(ca)) - break; - } - - blog(LOG_DEBUG, "coreaudio: exit the reconnect thread"); - ca->reconnecting = false; - return NULL; -} - -static void coreaudio_begin_reconnect(struct coreaudio_data *ca) -{ - int ret; - - if (ca->reconnecting) - return; - - ret = pthread_create(&ca->reconnect_thread, NULL, reconnect_thread, ca); - if (ret != 0) - blog(LOG_WARNING, - "[coreaudio_begin_reconnect] failed to " - "create thread, error code: %d", - ret); -} - -static OSStatus notification_callback(AudioObjectID id, UInt32 num_addresses, - const AudioObjectPropertyAddress addresses[], void *data) -{ - struct coreaudio_data *ca = data; - - coreaudio_stop(ca); - coreaudio_uninit(ca); - - if (addresses[0].mSelector == PROPERTY_DEFAULT_DEVICE) - ca->retry_time = 300; - else - ca->retry_time = 2000; - - blog(LOG_INFO, - "coreaudio: device '%s' disconnected or changed. " - "attempting to reconnect", - ca->device_name); - - coreaudio_begin_reconnect(ca); - - UNUSED_PARAMETER(id); - UNUSED_PARAMETER(num_addresses); - - return noErr; -} - -static OSStatus add_listener(struct coreaudio_data *ca, UInt32 property) -{ - AudioObjectPropertyAddress addr = {property, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; - - return AudioObjectAddPropertyListener(ca->device_id, &addr, notification_callback, ca); -} - -static bool coreaudio_init_hooks(struct coreaudio_data *ca) -{ - OSStatus stat; - AURenderCallbackStruct callback_info = {.inputProc = input_callback, .inputProcRefCon = ca}; - - stat = add_listener(ca, kAudioDevicePropertyDeviceIsAlive); - if (!ca_success(stat, ca, "coreaudio_init_hooks", "set disconnect callback")) - return false; - - stat = add_listener(ca, PROPERTY_FORMATS); - if (!ca_success(stat, ca, "coreaudio_init_hooks", "set format change callback")) - return false; - - if (ca->default_device) { - AudioObjectPropertyAddress addr = {PROPERTY_DEFAULT_DEVICE, kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMain}; - - stat = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &addr, notification_callback, ca); - if (!ca_success(stat, ca, "coreaudio_init_hooks", "set device change callback")) - return false; - } - - stat = set_property(ca->unit, kAudioOutputUnitProperty_SetInputCallback, SCOPE_GLOBAL, 0, &callback_info, - sizeof(callback_info)); - if (!ca_success(stat, ca, "coreaudio_init_hooks", "set input callback")) - return false; - - return true; -} - -static void coreaudio_remove_hooks(struct coreaudio_data *ca) -{ - AURenderCallbackStruct callback_info = {.inputProc = NULL, .inputProcRefCon = NULL}; - - AudioObjectPropertyAddress addr = {kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeGlobal, - kAudioObjectPropertyElementMain}; - - AudioObjectRemovePropertyListener(ca->device_id, &addr, notification_callback, ca); - - addr.mSelector = PROPERTY_FORMATS; - AudioObjectRemovePropertyListener(ca->device_id, &addr, notification_callback, ca); - - if (ca->default_device) { - addr.mSelector = PROPERTY_DEFAULT_DEVICE; - AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &addr, notification_callback, ca); - } - - set_property(ca->unit, kAudioOutputUnitProperty_SetInputCallback, SCOPE_GLOBAL, 0, &callback_info, - sizeof(callback_info)); -} - -static bool coreaudio_get_device_name(struct coreaudio_data *ca) -{ - CFStringRef cf_name = NULL; - UInt32 size = sizeof(CFStringRef); - char *name = NULL; - - const AudioObjectPropertyAddress addr = {kAudioDevicePropertyDeviceNameCFString, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyElementMain}; - - OSStatus stat = AudioObjectGetPropertyData(ca->device_id, &addr, 0, NULL, &size, &cf_name); - if (stat != noErr) { - blog(LOG_WARNING, - "[coreaudio_get_device_name] failed to " - "get name: %d", - (int)stat); - return false; - } - - name = cfstr_copy_cstr(cf_name, kCFStringEncodingUTF8); - if (!name) { - blog(LOG_WARNING, "[coreaudio_get_device_name] failed to " - "convert name to cstr for some reason"); - return false; - } - - bfree(ca->device_name); - ca->device_name = name; - - if (cf_name) - CFRelease(cf_name); - - return true; -} - -static bool coreaudio_start(struct coreaudio_data *ca) -{ - OSStatus stat; - - if (ca->active) - return true; - - stat = AudioOutputUnitStart(ca->unit); - return ca_success(stat, ca, "coreaudio_start", "start audio"); -} - -static void coreaudio_stop(struct coreaudio_data *ca) -{ - OSStatus stat; - - if (!ca->active) - return; - - ca->active = false; - - stat = AudioOutputUnitStop(ca->unit); - ca_success(stat, ca, "coreaudio_stop", "stop audio"); -} - -static bool coreaudio_init_unit(struct coreaudio_data *ca) -{ - AudioComponentDescription desc = {.componentType = kAudioUnitType_Output, - .componentSubType = kAudioUnitSubType_HALOutput}; - - AudioComponent component = AudioComponentFindNext(NULL, &desc); - if (!component) { - ca_warn(ca, "coreaudio_init_unit", "find component failed"); - return false; - } - - OSStatus stat = AudioComponentInstanceNew(component, &ca->unit); - if (!ca_success(stat, ca, "coreaudio_init_unit", "instance unit")) - return false; - - ca->au_initialized = true; - return true; -} - -static bool coreaudio_init(struct coreaudio_data *ca) -{ - OSStatus stat; - - if (ca->au_initialized) - return true; - - if (!find_device_id_by_uid(ca)) - return false; - if (!coreaudio_get_device_name(ca)) - return false; - if (!coreaudio_init_unit(ca)) - return false; - - stat = enable_io(ca, IO_TYPE_INPUT, true); - if (!ca_success(stat, ca, "coreaudio_init", "enable input io")) - goto fail; - - stat = enable_io(ca, IO_TYPE_OUTPUT, false); - if (!ca_success(stat, ca, "coreaudio_init", "disable output io")) - goto fail; - - stat = set_property(ca->unit, kAudioOutputUnitProperty_CurrentDevice, SCOPE_GLOBAL, 0, &ca->device_id, - sizeof(ca->device_id)); - if (!ca_success(stat, ca, "coreaudio_init", "set current device")) - goto fail; - - if (!coreaudio_init_format(ca)) - goto fail; - if (!coreaudio_init_buffer(ca)) - goto fail; - if (!coreaudio_init_hooks(ca)) - goto fail; - - stat = AudioUnitInitialize(ca->unit); - if (!ca_success(stat, ca, "coreaudio_initialize", "initialize")) - goto fail; - - if (!coreaudio_start(ca)) - goto fail; - - blog(LOG_INFO, "coreaudio: Device '%s' [%" PRIu32 " Hz] initialized", ca->device_name, ca->sample_rate); - return ca->au_initialized; - -fail: - coreaudio_uninit(ca); - return false; -} - -static void coreaudio_try_init(struct coreaudio_data *ca) -{ - if (!coreaudio_init(ca)) { - blog(LOG_INFO, - "coreaudio: failed to find device " - "uid: %s, waiting for connection", - ca->device_uid); - - ca->retry_time = 2000; - - if (ca->no_devices) - blog(LOG_INFO, "coreaudio: no device found"); - else - coreaudio_begin_reconnect(ca); - } -} - -static void coreaudio_uninit(struct coreaudio_data *ca) -{ - if (!ca->au_initialized) - return; - - if (ca->unit) { - coreaudio_stop(ca); - - OSStatus stat = AudioUnitUninitialize(ca->unit); - ca_success(stat, ca, "coreaudio_uninit", "uninitialize"); - - coreaudio_remove_hooks(ca); - - stat = AudioComponentInstanceDispose(ca->unit); - ca_success(stat, ca, "coreaudio_uninit", "dispose"); - - ca->unit = NULL; - } - - ca->au_initialized = false; - - buf_list_free(ca->buf_list); - ca->buf_list = NULL; - - if (ca->channel_names) { - for (uint32_t i = 0; i < ca->available_channels; i++) { - bfree(ca->channel_names[i]); - } - bfree(ca->channel_names); - ca->channel_names = NULL; - } -} - -/* ------------------------------------------------------------------------- */ - -static const char *coreaudio_input_getname(void *unused) -{ - UNUSED_PARAMETER(unused); - return TEXT_AUDIO_INPUT; -} - -static const char *coreaudio_output_getname(void *unused) -{ - UNUSED_PARAMETER(unused); - return TEXT_AUDIO_OUTPUT; -} - -static void coreaudio_shutdown(struct coreaudio_data *ca) -{ - if (ca->reconnecting) { - os_event_signal(ca->exit_event); - pthread_join(ca->reconnect_thread, NULL); - os_event_reset(ca->exit_event); - } - - coreaudio_uninit(ca); - - if (ca->unit) - AudioComponentInstanceDispose(ca->unit); -} - -static void coreaudio_destroy(void *data) -{ - struct coreaudio_data *ca = data; - - if (ca) { - coreaudio_shutdown(ca); - - os_event_destroy(ca->exit_event); - - if (ca->channel_map) { - bfree(ca->channel_map); - ca->channel_map = NULL; - } - - bfree(ca->device_name); - bfree(ca->device_uid); - bfree(ca); - } -} - -static void coreaudio_set_channels(struct coreaudio_data *ca, obs_data_t *settings) -{ - ca->channel_map = bzalloc(sizeof(SInt32) * MAX_AUDIO_CHANNELS); - - char *device_config_name = sanitize_device_name(ca->device_uid); - for (uint8_t i = 0; i < MAX_AUDIO_CHANNELS; i++) { - char setting_name[128]; - snprintf(setting_name, 128, "output-%s-%i", device_config_name, i + 1); - int64_t found = obs_data_has_user_value(settings, setting_name) - ? obs_data_get_int(settings, setting_name) - : -1L; - int64_t adjusted = found > 0 ? found - 1 : -1; - ca->channel_map[i] = (int32_t)adjusted; - } - bfree(device_config_name); -} - -static void coreaudio_update(void *data, obs_data_t *settings) -{ - struct coreaudio_data *ca = data; - - coreaudio_shutdown(ca); - - bfree(ca->device_uid); - ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id")); - - ca->enable_downmix = obs_data_get_bool(settings, "enable_downmix"); - - if (!ca->enable_downmix) { - coreaudio_set_channels(ca, settings); - } - - coreaudio_try_init(ca); -} - -static void coreaudio_defaults(obs_data_t *settings) -{ - obs_data_set_default_string(settings, "device_id", "default"); - obs_data_set_default_bool(settings, "enable_downmix", true); -} - -static void *coreaudio_create(obs_data_t *settings, obs_source_t *source, bool input) -{ - struct coreaudio_data *ca = bzalloc(sizeof(struct coreaudio_data)); - - if (os_event_init(&ca->exit_event, OS_EVENT_TYPE_MANUAL) != 0) { - blog(LOG_ERROR, - "[coreaudio_create] failed to create " - "semephore: %d", - errno); - bfree(ca); - return NULL; - } - - ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id")); - ca->source = source; - ca->input = input; - ca->enable_downmix = obs_data_get_bool(settings, "enable_downmix"); - - if (!ca->enable_downmix) { - coreaudio_set_channels(ca, settings); - } - - if (!ca->device_uid) - ca->device_uid = bstrdup("default"); - - coreaudio_try_init(ca); - return ca; -} - -static void *coreaudio_create_input_capture(obs_data_t *settings, obs_source_t *source) -{ - return coreaudio_create(settings, source, true); -} - -static void *coreaudio_create_output_capture(obs_data_t *settings, obs_source_t *source) -{ - return coreaudio_create(settings, source, false); -} - -static void coreaudio_fill_combo_with_inputs(const struct coreaudio_data *ca, obs_property_t *input_combo, - uint32_t output_channel) -{ - bool hasMutedChannel = false; - obs_property_list_clear(input_combo); - - if (output_channel < ca->available_channels) { - obs_property_list_add_int(input_combo, ca->channel_names[output_channel], output_channel + 1); - } else { - obs_property_list_add_int(input_combo, obs_module_text("CoreAudio.None"), -1); - hasMutedChannel = true; - } - - for (uint32_t input_chan = 0; input_chan < ca->available_channels; input_chan++) { - - if (input_chan != output_channel) { - obs_property_list_add_int(input_combo, ca->channel_names[input_chan], input_chan + 1); - } - } - - if (!hasMutedChannel) { - obs_property_list_add_int(input_combo, obs_module_text("CoreAudio.None"), -1); - } -} - -static void ensure_output_channel_prop(const struct coreaudio_data *ca, obs_properties_t *props, - const char *device_config_name, uint32_t out_chan) -{ - struct dstr name; - dstr_init(&name); - dstr_printf(&name, "output-%s-%d", device_config_name, out_chan + 1); - - obs_property_t *prop = obs_properties_get(props, name.array); - - if (prop) { - obs_property_set_visible(prop, true); - } else { - struct dstr label; - dstr_init(&label); - dstr_printf(&label, "%s %i", obs_module_text("CoreAudio.Channel"), out_chan + 1); - obs_property_t *input_combo = obs_properties_add_list(props, name.array, label.array, - OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); - dstr_free(&label); - coreaudio_fill_combo_with_inputs(ca, input_combo, out_chan); - } - dstr_free(&name); -} - -static void ensure_output_channels_visible(obs_properties_t *props, const struct coreaudio_data *ca, uint32_t channels) -{ - char *device_config_name = sanitize_device_name(ca->device_uid); - for (uint32_t out_chan = 0; out_chan < channels; out_chan++) { - ensure_output_channel_prop(ca, props, device_config_name, out_chan); - } - bfree(device_config_name); -} - -static void hide_all_output_channels(obs_properties_t *props) -{ - for (obs_property_t *prop = obs_properties_first(props); prop != NULL; obs_property_next(&prop)) { - const char *prop_name = obs_property_name(prop); - if (strncmp("output-", prop_name, 7) == 0) { - obs_property_set_visible(prop, false); - } - } -} - -static bool coreaudio_device_changed(void *data, obs_properties_t *props, obs_property_t *p, obs_data_t *settings) -{ - struct coreaudio_data *ca = data; - if (ca != NULL) { - hide_all_output_channels(props); - - if (!ca->enable_downmix) { - uint32_t channels = get_audio_channels(ca->speakers); - ensure_output_channels_visible(props, ca, channels); - } - } - UNUSED_PARAMETER(p); - UNUSED_PARAMETER(settings); - return true; -} - -static bool coreaudio_downmix_changed(void *data, obs_properties_t *props, obs_property_t *p __unused, - obs_data_t *settings) -{ - struct coreaudio_data *ca = data; - if (ca != NULL) { - bool enable_downmix = obs_data_get_bool(settings, "enable_downmix"); - ca->enable_downmix = enable_downmix; - - hide_all_output_channels(props); - - if (!ca->enable_downmix) { - uint32_t channels = get_audio_channels(ca->speakers); - ensure_output_channels_visible(props, ca, channels); - } - } - - return true; -} - -static obs_properties_t *coreaudio_properties(bool input, void *data) -{ - struct coreaudio_data *ca = data; - obs_properties_t *props = obs_properties_create(); - obs_property_t *property; - struct device_list devices; - - memset(&devices, 0, sizeof(struct device_list)); - - property = - obs_properties_add_list(props, "device_id", TEXT_DEVICE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); - - coreaudio_enum_devices(&devices, input); - - if (devices.items.num) - obs_property_list_add_string(property, TEXT_DEVICE_DEFAULT, "default"); - - for (size_t i = 0; i < devices.items.num; i++) { - struct device_item *item = devices.items.array + i; - obs_property_list_add_string(property, item->name.array, item->value.array); - } - - obs_property_set_modified_callback2(property, coreaudio_device_changed, ca); - - property = obs_properties_add_bool(props, "enable_downmix", obs_module_text("CoreAudio.Downmix")); - obs_property_set_modified_callback2(property, coreaudio_downmix_changed, ca); - - if (ca != NULL && ca->au_initialized) { - uint32_t channels = get_audio_channels(ca->speakers); - ensure_output_channels_visible(props, ca, channels); - - if (ca->enable_downmix) { - hide_all_output_channels(props); - } - } - - device_list_free(&devices); - return props; -} - -static obs_properties_t *coreaudio_input_properties(void *data) -{ - return coreaudio_properties(true, data); -} - -static obs_properties_t *coreaudio_output_properties(void *data) -{ - return coreaudio_properties(false, data); -} - -struct obs_source_info coreaudio_input_capture_info = { - .id = "coreaudio_input_capture", - .type = OBS_SOURCE_TYPE_INPUT, - .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE, - .get_name = coreaudio_input_getname, - .create = coreaudio_create_input_capture, - .destroy = coreaudio_destroy, - .update = coreaudio_update, - .get_defaults = coreaudio_defaults, - .get_properties = coreaudio_input_properties, - .icon_type = OBS_ICON_TYPE_AUDIO_INPUT, -}; - -struct obs_source_info coreaudio_output_capture_info = { - .id = "coreaudio_output_capture", - .type = OBS_SOURCE_TYPE_INPUT, - .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_DO_NOT_SELF_MONITOR, - .get_name = coreaudio_output_getname, - .create = coreaudio_create_output_capture, - .destroy = coreaudio_destroy, - .update = coreaudio_update, - .get_defaults = coreaudio_defaults, - .get_properties = coreaudio_output_properties, - .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT, -}; diff --git a/plugins/mac-capture/mac-audio.m b/plugins/mac-capture/mac-audio.m new file mode 100644 index 00000000000000..c56eb5090f8cfc --- /dev/null +++ b/plugins/mac-capture/mac-audio.m @@ -0,0 +1,1073 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "audio-device-enum.h" + +#define PROPERTY_DEFAULT_DEVICE kAudioHardwarePropertyDefaultInputDevice +#define PROPERTY_FORMATS kAudioStreamPropertyAvailablePhysicalFormats + +#define SCOPE_OUTPUT kAudioUnitScope_Output +#define SCOPE_INPUT kAudioUnitScope_Input +#define SCOPE_GLOBAL kAudioUnitScope_Global + +#define BUS_OUTPUT 0 +#define BUS_INPUT 1 + +#define set_property AudioUnitSetProperty +#define get_property AudioUnitGetProperty + +#define TEXT_AUDIO_INPUT obs_module_text("CoreAudio.InputCapture"); +#define TEXT_AUDIO_OUTPUT obs_module_text("CoreAudio.OutputCapture"); +#define TEXT_DEVICE obs_module_text("CoreAudio.Device") +#define TEXT_DEVICE_DEFAULT obs_module_text("CoreAudio.Device.Default") + +struct coreaudio_data { + char *device_name; + char *device_uid; + AudioUnit unit; + AudioDeviceID device_id; + AudioBufferList *buf_list; + bool au_initialized; + bool active; + bool default_device; + bool input; + bool no_devices; + + uint32_t available_channels; + char **channel_names; + int32_t *channel_map; + + uint32_t sample_rate; + enum audio_format format; + enum speaker_layout speakers; + bool enable_downmix; + + char *effect_warning; + + pthread_t reconnect_thread; + os_event_t *exit_event; + volatile bool reconnecting; + unsigned long retry_time; + + obs_source_t *source; +}; + +static bool get_default_output_device(struct coreaudio_data *ca) +{ + struct device_list list; + + memset(&list, 0, sizeof(struct device_list)); + coreaudio_enum_devices(&list, false); + + if (!list.items.num) + return false; + + bfree(ca->device_uid); + ca->device_uid = bstrdup(list.items.array[0].value.array); + + device_list_free(&list); + return true; +} + +static bool find_device_id_by_uid(struct coreaudio_data *ca) +{ + UInt32 size = sizeof(AudioDeviceID); + CFStringRef cf_uid = NULL; + CFStringRef qual = NULL; + UInt32 qual_size = 0; + OSStatus stat; + bool success; + + AudioObjectPropertyAddress addr = {.mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMain}; + + if (!ca->device_uid) + ca->device_uid = bstrdup("default"); + + ca->default_device = false; + ca->no_devices = false; + + /* have to do this because mac output devices don't actually exist */ + if (astrcmpi(ca->device_uid, "default") == 0) { + if (ca->input) { + ca->default_device = true; + } else { + if (!get_default_output_device(ca)) { + ca->no_devices = true; + return false; + } + } + } + + cf_uid = CFStringCreateWithCString(NULL, ca->device_uid, kCFStringEncodingUTF8); + + if (ca->default_device) { + addr.mSelector = kAudioHardwarePropertyDefaultInputDevice; + stat = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, qual_size, &qual, &size, &ca->device_id); + success = (stat == noErr); + } else { + success = coreaudio_get_device_id(cf_uid, &ca->device_id); + } + + if (cf_uid) + CFRelease(cf_uid); + + return success; +} + +static inline void ca_warn(struct coreaudio_data *ca, const char *func, const char *format, ...) +{ + va_list args; + struct dstr str = {0}; + + va_start(args, format); + + dstr_printf(&str, "[%s]:[device '%s'] ", func, ca->device_name); + dstr_vcatf(&str, format, args); + blog(LOG_WARNING, "%s", str.array); + dstr_free(&str); + + va_end(args); +} + +static inline bool ca_success(OSStatus stat, struct coreaudio_data *ca, const char *func, const char *action) +{ + if (stat != noErr) { + blog(LOG_WARNING, "[%s]:[device '%s'] %s failed: %d", func, ca->device_name, action, (int) stat); + return false; + } + + return true; +} + +enum coreaudio_io_type { + IO_TYPE_INPUT, + IO_TYPE_OUTPUT, +}; + +static inline bool enable_io(struct coreaudio_data *ca, enum coreaudio_io_type type, bool enable) +{ + UInt32 enable_int = enable; + return set_property(ca->unit, kAudioOutputUnitProperty_EnableIO, + (type == IO_TYPE_INPUT) ? SCOPE_INPUT : SCOPE_OUTPUT, + (type == IO_TYPE_INPUT) ? BUS_INPUT : BUS_OUTPUT, &enable_int, sizeof(enable_int)); +} + +static inline enum speaker_layout convert_ca_speaker_layout(UInt32 channels) +{ + switch (channels) { + case 1: + return SPEAKERS_MONO; + case 2: + return SPEAKERS_STEREO; + case 3: + return SPEAKERS_2POINT1; + case 4: + return SPEAKERS_4POINT0; + case 5: + return SPEAKERS_4POINT1; + case 6: + return SPEAKERS_5POINT1; + case 8: + return SPEAKERS_7POINT1; + } + return SPEAKERS_UNKNOWN; +} + +static inline enum audio_format convert_ca_format(UInt32 format_flags, UInt32 bits) +{ + bool planar = (format_flags & kAudioFormatFlagIsNonInterleaved) != 0; + + if (format_flags & kAudioFormatFlagIsFloat) + return planar ? AUDIO_FORMAT_FLOAT_PLANAR : AUDIO_FORMAT_FLOAT; + + if (!(format_flags & kAudioFormatFlagIsSignedInteger) && bits == 8) + return planar ? AUDIO_FORMAT_U8BIT_PLANAR : AUDIO_FORMAT_U8BIT; + + /* not float? not signed int? no clue, fail */ + if ((format_flags & kAudioFormatFlagIsSignedInteger) == 0) + return AUDIO_FORMAT_UNKNOWN; + + if (bits == 16) + return planar ? AUDIO_FORMAT_16BIT_PLANAR : AUDIO_FORMAT_16BIT; + else if (bits == 32) + return planar ? AUDIO_FORMAT_32BIT_PLANAR : AUDIO_FORMAT_32BIT; + + return AUDIO_FORMAT_UNKNOWN; +} + +static char *sanitize_device_name(char *name) +{ + const size_t max_len = 64; + size_t len = strlen(name); + char buf[64]; + size_t out_idx = 0; + + for (size_t i = len > max_len ? len - max_len : 0; i < len; i++) { + char c = name[i]; + if (isalnum(c)) { + buf[out_idx++] = name[i]; + } + if (c == '-' || c == ' ' || c == '_' || c == ':') { + buf[out_idx++] = '_'; + } + } + return bstrdup_n(buf, out_idx); +} + +static char **coreaudio_get_channel_names(struct coreaudio_data *ca) +{ + char **channel_names = bzalloc(sizeof(char *) * ca->available_channels); + + for (uint32_t i = 0; i < ca->available_channels; i++) { + CFStringRef cf_chan_name = NULL; + UInt32 dataSize = sizeof(cf_chan_name); + AudioObjectPropertyAddress pa; + pa.mSelector = kAudioObjectPropertyElementName; + pa.mScope = kAudioDevicePropertyScopeInput; + pa.mElement = i + 1; + OSStatus stat = AudioObjectGetPropertyData(ca->device_id, &pa, 0, NULL, &dataSize, &cf_chan_name); + + struct dstr name; + dstr_init(&name); + if (ca_success(stat, ca, "coreaudio_init_format", "get channel names") && CFStringGetLength(cf_chan_name)) { + char *channelName = cfstr_copy_cstr(cf_chan_name, kCFStringEncodingUTF8); + + dstr_printf(&name, "%s", channelName); + + if (channelName) { + bfree(channelName); + } + } else { + dstr_printf(&name, "%s %d", obs_module_text("CoreAudio.Channel.Device"), i + 1); + } + channel_names[i] = bstrdup_n(name.array, name.len); + dstr_free(&name); + + if (cf_chan_name) { + CFRelease(cf_chan_name); + } + } + return channel_names; +} + +static bool coreaudio_init_format(struct coreaudio_data *ca) +{ + AudioStreamBasicDescription desc; + AudioStreamBasicDescription inputDescription; + OSStatus stat; + UInt32 size; + struct obs_audio_info aoi; + if (!obs_get_audio_info(&aoi)) { + blog(LOG_WARNING, "No active audio"); + return false; + } + ca->speakers = aoi.speakers; + uint32_t channels = get_audio_channels(ca->speakers); + + size = sizeof(inputDescription); + stat = get_property(ca->unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &inputDescription, &size); + + if (!ca_success(stat, ca, "coreaudio_init_format", "get input device format")) + return false; + + stat = get_property(ca->unit, kAudioUnitProperty_StreamFormat, SCOPE_OUTPUT, BUS_INPUT, &desc, &size); + if (!ca_success(stat, ca, "coreaudio_init_format", "get input format")) + return false; + + ca->available_channels = inputDescription.mChannelsPerFrame; + if (ca->available_channels > MAX_DEVICE_INPUT_CHANNELS) { + ca->available_channels = MAX_DEVICE_INPUT_CHANNELS; + } + + ca->channel_names = coreaudio_get_channel_names(ca); + + if (ca->enable_downmix) { + blog(LOG_INFO, "Downmix enabled: %d to %d channels.", ca->available_channels, channels); + desc.mChannelsPerFrame = ca->available_channels; + } else { + // Mute any channels mapped in config that we don't really have + char *sep = ""; + struct dstr cm_str; + dstr_init(&cm_str); + for (size_t i = 0; i < channels; i++) { + dstr_cat(&cm_str, sep); + if (ca->channel_map[i] >= (int32_t) ca->available_channels) { + ca->channel_map[i] = -1; + } + dstr_catf(&cm_str, "%d", ca->channel_map[i]); + sep = ","; + } + blog(LOG_INFO, "Channel map enabled: [%s] (%d channels available)", cm_str.array, ca->available_channels); + dstr_free(&cm_str); + + stat = set_property(ca->unit, kAudioOutputUnitProperty_ChannelMap, SCOPE_OUTPUT, BUS_INPUT, ca->channel_map, + sizeof(SInt32) * channels); + if (!ca_success(stat, ca, "coreaudio_init_format", "set channel map")) { + return false; + } + + desc.mChannelsPerFrame = channels; + } + + desc.mSampleRate = inputDescription.mSampleRate; + + stat = set_property(ca->unit, kAudioUnitProperty_StreamFormat, SCOPE_OUTPUT, BUS_INPUT, &desc, size); + if (!ca_success(stat, ca, "coreaudio_init_format", "set output format")) + return false; + + if (desc.mFormatID != kAudioFormatLinearPCM) { + ca_warn(ca, "coreaudio_init_format", "format is not PCM"); + return false; + } + + ca->format = convert_ca_format(desc.mFormatFlags, desc.mBitsPerChannel); + if (ca->format == AUDIO_FORMAT_UNKNOWN) { + ca_warn(ca, "coreaudio_init_format", + "unknown format flags: " + "%u, bits: %u", + (unsigned int) desc.mFormatFlags, (unsigned int) desc.mBitsPerChannel); + return false; + } + + ca->sample_rate = (uint32_t) desc.mSampleRate; + + return true; +} + +static bool coreaudio_init_buffer(struct coreaudio_data *ca) +{ + UInt32 bufferSizeFrames; + UInt32 bufferSizeBytes; + UInt32 propertySize; + OSStatus err = noErr; + + propertySize = sizeof(bufferSizeFrames); + err = AudioUnitGetProperty(ca->unit, kAudioDevicePropertyBufferFrameSize, kAudioUnitScope_Global, 0, + &bufferSizeFrames, &propertySize); + + if (!ca_success(err, ca, "coreaudio_init_buffer", "get buffer frame size")) { + return false; + } + + bufferSizeBytes = bufferSizeFrames * sizeof(Float32); + + AudioStreamBasicDescription streamDescription; + propertySize = sizeof(streamDescription); + err = AudioUnitGetProperty(ca->unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &streamDescription, + &propertySize); + + if (!ca_success(err, ca, "coreaudio_init_buffer", "get stream format")) { + return false; + } + + if (!ca->enable_downmix) { + streamDescription.mChannelsPerFrame = get_audio_channels(ca->speakers); + } + + Float64 rate = 0.0; + propertySize = sizeof(Float64); + AudioObjectPropertyAddress propertyAddress = {kAudioDevicePropertyNominalSampleRate, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; + + err = AudioObjectGetPropertyData(ca->device_id, &propertyAddress, 0, NULL, &propertySize, &rate); + + if (!ca_success(err, ca, "coreaudio_init_buffer", "get input sample rate")) { + return false; + } + + streamDescription.mSampleRate = rate; + + int bufferPropertySize = + offsetof(AudioBufferList, mBuffers[0]) + (sizeof(AudioBuffer) * streamDescription.mChannelsPerFrame); + + AudioBufferList *inputBuffer = (AudioBufferList *) bmalloc(bufferPropertySize); + inputBuffer->mNumberBuffers = streamDescription.mChannelsPerFrame; + + for (UInt32 i = 0; i < inputBuffer->mNumberBuffers; i++) { + inputBuffer->mBuffers[i].mNumberChannels = 1; + inputBuffer->mBuffers[i].mDataByteSize = bufferSizeBytes; + inputBuffer->mBuffers[i].mData = bmalloc(bufferSizeBytes); + } + + ca->buf_list = inputBuffer; + return true; +} + +static void buf_list_free(AudioBufferList *buf_list) +{ + if (buf_list) { + for (UInt32 i = 0; i < buf_list->mNumberBuffers; i++) + bfree(buf_list->mBuffers[i].mData); + + bfree(buf_list); + } +} + +static OSStatus input_callback(void *data, AudioUnitRenderActionFlags *action_flags, const AudioTimeStamp *ts_data, + UInt32 bus_num, UInt32 frames, AudioBufferList *ignored_buffers) +{ + struct coreaudio_data *ca = data; + OSStatus stat; + struct obs_source_audio audio; + + stat = AudioUnitRender(ca->unit, action_flags, ts_data, bus_num, frames, ca->buf_list); + if (!ca_success(stat, ca, "input_callback", "audio retrieval")) + return noErr; + + for (UInt32 i = 0; i < ca->buf_list->mNumberBuffers; i++) { + if (i < MAX_AUDIO_CHANNELS) { + audio.data[i] = ca->buf_list->mBuffers[i].mData; + } + } + + audio.frames = frames; + audio.speakers = (ca->buf_list->mNumberBuffers > MAX_AUDIO_CHANNELS) ? MAX_AUDIO_CHANNELS + : ca->buf_list->mNumberBuffers; + audio.format = ca->format; + audio.samples_per_sec = ca->sample_rate; + static double factor = 0.; + static mach_timebase_info_data_t info = {0, 0}; + if (info.numer == 0 && info.denom == 0) { + mach_timebase_info(&info); + factor = ((double) info.numer) / info.denom; + } + if (info.numer != info.denom) + audio.timestamp = (uint64_t) (factor * ts_data->mHostTime); + else + audio.timestamp = ts_data->mHostTime; + + obs_source_output_audio(ca->source, &audio); + + UNUSED_PARAMETER(ignored_buffers); + return noErr; +} + +static void coreaudio_stop(struct coreaudio_data *ca); +static bool coreaudio_init(struct coreaudio_data *ca); +static void coreaudio_uninit(struct coreaudio_data *ca); + +static void *reconnect_thread(void *param) +{ + struct coreaudio_data *ca = param; + + ca->reconnecting = true; + + while (os_event_timedwait(ca->exit_event, ca->retry_time) == ETIMEDOUT) { + if (coreaudio_init(ca)) + break; + } + + blog(LOG_DEBUG, "coreaudio: exit the reconnect thread"); + ca->reconnecting = false; + return NULL; +} + +static void coreaudio_begin_reconnect(struct coreaudio_data *ca) +{ + int ret; + + if (ca->reconnecting) + return; + + ret = pthread_create(&ca->reconnect_thread, NULL, reconnect_thread, ca); + if (ret != 0) + blog(LOG_WARNING, + "[coreaudio_begin_reconnect] failed to " + "create thread, error code: %d", + ret); +} + +static OSStatus notification_callback(AudioObjectID id, UInt32 num_addresses, + const AudioObjectPropertyAddress addresses[], void *data) +{ + struct coreaudio_data *ca = data; + + coreaudio_stop(ca); + coreaudio_uninit(ca); + + if (addresses[0].mSelector == PROPERTY_DEFAULT_DEVICE) + ca->retry_time = 300; + else + ca->retry_time = 2000; + + blog(LOG_INFO, + "coreaudio: device '%s' disconnected or changed. " + "attempting to reconnect", + ca->device_name); + + coreaudio_begin_reconnect(ca); + + UNUSED_PARAMETER(id); + UNUSED_PARAMETER(num_addresses); + + return noErr; +} + +static OSStatus add_listener(struct coreaudio_data *ca, UInt32 property) +{ + AudioObjectPropertyAddress addr = {property, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain}; + + return AudioObjectAddPropertyListener(ca->device_id, &addr, notification_callback, ca); +} + +static bool coreaudio_init_hooks(struct coreaudio_data *ca) +{ + OSStatus stat; + AURenderCallbackStruct callback_info = {.inputProc = input_callback, .inputProcRefCon = ca}; + + stat = add_listener(ca, kAudioDevicePropertyDeviceIsAlive); + if (!ca_success(stat, ca, "coreaudio_init_hooks", "set disconnect callback")) + return false; + + stat = add_listener(ca, PROPERTY_FORMATS); + if (!ca_success(stat, ca, "coreaudio_init_hooks", "set format change callback")) + return false; + + if (ca->default_device) { + AudioObjectPropertyAddress addr = {PROPERTY_DEFAULT_DEVICE, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain}; + + stat = AudioObjectAddPropertyListener(kAudioObjectSystemObject, &addr, notification_callback, ca); + if (!ca_success(stat, ca, "coreaudio_init_hooks", "set device change callback")) + return false; + } + + stat = set_property(ca->unit, kAudioOutputUnitProperty_SetInputCallback, SCOPE_GLOBAL, 0, &callback_info, + sizeof(callback_info)); + if (!ca_success(stat, ca, "coreaudio_init_hooks", "set input callback")) + return false; + + return true; +} + +static void coreaudio_remove_hooks(struct coreaudio_data *ca) +{ + AURenderCallbackStruct callback_info = {.inputProc = NULL, .inputProcRefCon = NULL}; + + AudioObjectPropertyAddress addr = {kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyElementMain}; + + AudioObjectRemovePropertyListener(ca->device_id, &addr, notification_callback, ca); + + addr.mSelector = PROPERTY_FORMATS; + AudioObjectRemovePropertyListener(ca->device_id, &addr, notification_callback, ca); + + if (ca->default_device) { + addr.mSelector = PROPERTY_DEFAULT_DEVICE; + AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &addr, notification_callback, ca); + } + + set_property(ca->unit, kAudioOutputUnitProperty_SetInputCallback, SCOPE_GLOBAL, 0, &callback_info, + sizeof(callback_info)); +} + +static bool coreaudio_get_device_name(struct coreaudio_data *ca) +{ + CFStringRef cf_name = NULL; + UInt32 size = sizeof(CFStringRef); + char *name = NULL; + + const AudioObjectPropertyAddress addr = {kAudioDevicePropertyDeviceNameCFString, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyElementMain}; + + OSStatus stat = AudioObjectGetPropertyData(ca->device_id, &addr, 0, NULL, &size, &cf_name); + if (stat != noErr) { + blog(LOG_WARNING, + "[coreaudio_get_device_name] failed to " + "get name: %d", + (int) stat); + return false; + } + + name = cfstr_copy_cstr(cf_name, kCFStringEncodingUTF8); + if (!name) { + blog(LOG_WARNING, "[coreaudio_get_device_name] failed to " + "convert name to cstr for some reason"); + return false; + } + + bfree(ca->device_name); + ca->device_name = name; + + if (cf_name) + CFRelease(cf_name); + + return true; +} + +static bool coreaudio_start(struct coreaudio_data *ca) +{ + OSStatus stat; + + if (ca->active) + return true; + + stat = AudioOutputUnitStart(ca->unit); + return ca_success(stat, ca, "coreaudio_start", "start audio"); +} + +static void coreaudio_stop(struct coreaudio_data *ca) +{ + OSStatus stat; + + if (!ca->active) + return; + + ca->active = false; + + stat = AudioOutputUnitStop(ca->unit); + ca_success(stat, ca, "coreaudio_stop", "stop audio"); +} + +static bool coreaudio_init_unit(struct coreaudio_data *ca) +{ + AudioComponentDescription desc = {.componentType = kAudioUnitType_Output, + .componentSubType = kAudioUnitSubType_HALOutput}; + + AudioComponent component = AudioComponentFindNext(NULL, &desc); + if (!component) { + ca_warn(ca, "coreaudio_init_unit", "find component failed"); + return false; + } + + OSStatus stat = AudioComponentInstanceNew(component, &ca->unit); + if (!ca_success(stat, ca, "coreaudio_init_unit", "instance unit")) + return false; + + ca->au_initialized = true; + return true; +} + +static bool coreaudio_init(struct coreaudio_data *ca) +{ + OSStatus stat; + + if (ca->au_initialized) + return true; + + if (!find_device_id_by_uid(ca)) + return false; + if (!coreaudio_get_device_name(ca)) + return false; + if (!coreaudio_init_unit(ca)) + return false; + + stat = enable_io(ca, IO_TYPE_INPUT, true); + if (!ca_success(stat, ca, "coreaudio_init", "enable input io")) + goto fail; + + stat = enable_io(ca, IO_TYPE_OUTPUT, false); + if (!ca_success(stat, ca, "coreaudio_init", "disable output io")) + goto fail; + + stat = set_property(ca->unit, kAudioOutputUnitProperty_CurrentDevice, SCOPE_GLOBAL, 0, &ca->device_id, + sizeof(ca->device_id)); + if (!ca_success(stat, ca, "coreaudio_init", "set current device")) + goto fail; + + if (!coreaudio_init_format(ca)) + goto fail; + if (!coreaudio_init_buffer(ca)) + goto fail; + if (!coreaudio_init_hooks(ca)) + goto fail; + + stat = AudioUnitInitialize(ca->unit); + if (!ca_success(stat, ca, "coreaudio_initialize", "initialize")) + goto fail; + + if (!coreaudio_start(ca)) + goto fail; + + blog(LOG_INFO, "coreaudio: Device '%s' [%" PRIu32 " Hz] initialized", ca->device_name, ca->sample_rate); + return ca->au_initialized; + +fail: + coreaudio_uninit(ca); + return false; +} + +static void coreaudio_try_init(struct coreaudio_data *ca) +{ + if (!coreaudio_init(ca)) { + blog(LOG_INFO, + "coreaudio: failed to find device " + "uid: %s, waiting for connection", + ca->device_uid); + + ca->retry_time = 2000; + + if (ca->no_devices) + blog(LOG_INFO, "coreaudio: no device found"); + else + coreaudio_begin_reconnect(ca); + } +} + +static void coreaudio_uninit(struct coreaudio_data *ca) +{ + if (!ca->au_initialized) + return; + + if (ca->unit) { + coreaudio_stop(ca); + + OSStatus stat = AudioUnitUninitialize(ca->unit); + ca_success(stat, ca, "coreaudio_uninit", "uninitialize"); + + coreaudio_remove_hooks(ca); + + stat = AudioComponentInstanceDispose(ca->unit); + ca_success(stat, ca, "coreaudio_uninit", "dispose"); + + ca->unit = NULL; + } + + ca->au_initialized = false; + + buf_list_free(ca->buf_list); + ca->buf_list = NULL; + + if (ca->channel_names) { + for (uint32_t i = 0; i < ca->available_channels; i++) { + bfree(ca->channel_names[i]); + } + bfree(ca->channel_names); + ca->channel_names = NULL; + } +} + +/* ------------------------------------------------------------------------- */ + +static const char *coreaudio_input_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return TEXT_AUDIO_INPUT; +} + +static const char *coreaudio_output_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return TEXT_AUDIO_OUTPUT; +} + +static void coreaudio_shutdown(struct coreaudio_data *ca) +{ + if (ca->reconnecting) { + os_event_signal(ca->exit_event); + pthread_join(ca->reconnect_thread, NULL); + os_event_reset(ca->exit_event); + } + + coreaudio_uninit(ca); + + if (ca->unit) + AudioComponentInstanceDispose(ca->unit); +} + +static void coreaudio_destroy(void *data) +{ + struct coreaudio_data *ca = data; + + if (ca) { + coreaudio_shutdown(ca); + + os_event_destroy(ca->exit_event); + + if (ca->channel_map) { + bfree(ca->channel_map); + ca->channel_map = NULL; + } + + bfree(ca->device_name); + bfree(ca->device_uid); + bfree(ca); + } +} + +static void coreaudio_set_channels(struct coreaudio_data *ca, obs_data_t *settings) +{ + ca->channel_map = bzalloc(sizeof(SInt32) * MAX_AUDIO_CHANNELS); + + char *device_config_name = sanitize_device_name(ca->device_uid); + for (uint8_t i = 0; i < MAX_AUDIO_CHANNELS; i++) { + char setting_name[128]; + snprintf(setting_name, 128, "output-%s-%i", device_config_name, i + 1); + int64_t found = obs_data_has_user_value(settings, setting_name) ? obs_data_get_int(settings, setting_name) + : -1L; + int64_t adjusted = found > 0 ? found - 1 : -1; + ca->channel_map[i] = (int32_t) adjusted; + } + bfree(device_config_name); +} + +static void coreaudio_update(void *data, obs_data_t *settings) +{ + struct coreaudio_data *ca = data; + + coreaudio_shutdown(ca); + + bfree(ca->device_uid); + ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id")); + + ca->enable_downmix = obs_data_get_bool(settings, "enable_downmix"); + + if (!ca->enable_downmix) { + coreaudio_set_channels(ca, settings); + } + + coreaudio_try_init(ca); +} + +static void coreaudio_defaults(obs_data_t *settings) +{ + obs_data_set_default_string(settings, "device_id", "default"); + obs_data_set_default_bool(settings, "enable_downmix", true); +} + +static void *coreaudio_create(obs_data_t *settings, obs_source_t *source, bool input) +{ + struct coreaudio_data *ca = bzalloc(sizeof(struct coreaudio_data)); + + if (os_event_init(&ca->exit_event, OS_EVENT_TYPE_MANUAL) != 0) { + blog(LOG_ERROR, + "[coreaudio_create] failed to create " + "semephore: %d", + errno); + bfree(ca); + return NULL; + } + + ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id")); + ca->source = source; + ca->input = input; + ca->enable_downmix = obs_data_get_bool(settings, "enable_downmix"); + + if (!ca->enable_downmix) { + coreaudio_set_channels(ca, settings); + } + + if (!ca->device_uid) + ca->device_uid = bstrdup("default"); + + coreaudio_try_init(ca); + return ca; +} + +static void *coreaudio_create_input_capture(obs_data_t *settings, obs_source_t *source) +{ + return coreaudio_create(settings, source, true); +} + +static void *coreaudio_create_output_capture(obs_data_t *settings, obs_source_t *source) +{ + return coreaudio_create(settings, source, false); +} + +static void coreaudio_fill_combo_with_inputs(const struct coreaudio_data *ca, obs_property_t *input_combo, + uint32_t output_channel) +{ + bool hasMutedChannel = false; + obs_property_list_clear(input_combo); + + if (output_channel < ca->available_channels) { + obs_property_list_add_int(input_combo, ca->channel_names[output_channel], output_channel + 1); + } else { + obs_property_list_add_int(input_combo, obs_module_text("CoreAudio.None"), -1); + hasMutedChannel = true; + } + + for (uint32_t input_chan = 0; input_chan < ca->available_channels; input_chan++) { + if (input_chan != output_channel) { + obs_property_list_add_int(input_combo, ca->channel_names[input_chan], input_chan + 1); + } + } + + if (!hasMutedChannel) { + obs_property_list_add_int(input_combo, obs_module_text("CoreAudio.None"), -1); + } +} + +static void ensure_output_channel_prop(const struct coreaudio_data *ca, obs_properties_t *props, + const char *device_config_name, uint32_t out_chan) +{ + struct dstr name; + dstr_init(&name); + dstr_printf(&name, "output-%s-%d", device_config_name, out_chan + 1); + + obs_property_t *prop = obs_properties_get(props, name.array); + + if (prop) { + obs_property_set_visible(prop, true); + } else { + struct dstr label; + dstr_init(&label); + dstr_printf(&label, "%s %i", obs_module_text("CoreAudio.Channel"), out_chan + 1); + obs_property_t *input_combo = + obs_properties_add_list(props, name.array, label.array, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); + dstr_free(&label); + coreaudio_fill_combo_with_inputs(ca, input_combo, out_chan); + } + dstr_free(&name); +} + +static void ensure_output_channels_visible(obs_properties_t *props, const struct coreaudio_data *ca, uint32_t channels) +{ + char *device_config_name = sanitize_device_name(ca->device_uid); + for (uint32_t out_chan = 0; out_chan < channels; out_chan++) { + ensure_output_channel_prop(ca, props, device_config_name, out_chan); + } + bfree(device_config_name); +} + +static void hide_all_output_channels(obs_properties_t *props) +{ + for (obs_property_t *prop = obs_properties_first(props); prop != NULL; obs_property_next(&prop)) { + const char *prop_name = obs_property_name(prop); + if (strncmp("output-", prop_name, 7) == 0) { + obs_property_set_visible(prop, false); + } + } +} + +static bool coreaudio_device_changed(void *data, obs_properties_t *props, obs_property_t *p, obs_data_t *settings) +{ + struct coreaudio_data *ca = data; + if (ca != NULL) { + hide_all_output_channels(props); + + if (!ca->enable_downmix) { + uint32_t channels = get_audio_channels(ca->speakers); + ensure_output_channels_visible(props, ca, channels); + } + } + UNUSED_PARAMETER(p); + UNUSED_PARAMETER(settings); + return true; +} + +static bool coreaudio_downmix_changed(void *data, obs_properties_t *props, obs_property_t *p __unused, + obs_data_t *settings) +{ + struct coreaudio_data *ca = data; + if (ca != NULL) { + bool enable_downmix = obs_data_get_bool(settings, "enable_downmix"); + ca->enable_downmix = enable_downmix; + + hide_all_output_channels(props); + + if (!ca->enable_downmix) { + uint32_t channels = get_audio_channels(ca->speakers); + ensure_output_channels_visible(props, ca, channels); + } + } + + return true; +} + +static obs_properties_t *coreaudio_properties(bool input, void *data) +{ + struct coreaudio_data *ca = data; + obs_properties_t *props = obs_properties_create(); + obs_property_t *property; + struct device_list devices; + + memset(&devices, 0, sizeof(struct device_list)); + + property = obs_properties_add_list(props, "device_id", TEXT_DEVICE, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + coreaudio_enum_devices(&devices, input); + + if (devices.items.num) + obs_property_list_add_string(property, TEXT_DEVICE_DEFAULT, "default"); + + for (size_t i = 0; i < devices.items.num; i++) { + struct device_item *item = devices.items.array + i; + obs_property_list_add_string(property, item->name.array, item->value.array); + } + + obs_property_set_modified_callback2(property, coreaudio_device_changed, ca); + + // This file is compiled as Objective-C so that we may more easily check this class property + if (@available(macOS 12.0, *)) { + AVCaptureMicrophoneMode activeMicrophoneMode = AVCaptureDevice.activeMicrophoneMode; + NSString *effectWarningKey = nil; + switch (activeMicrophoneMode) { + case AVCaptureMicrophoneModeStandard: + break; + case AVCaptureMicrophoneModeWideSpectrum: + effectWarningKey = @"Warning.Effect.Audio.WideSpectrum"; + break; + case AVCaptureMicrophoneModeVoiceIsolation: + effectWarningKey = @"Warning.Effect.Audio.VoiceIsolation"; + break; + } + if (effectWarningKey) { + property = obs_properties_add_text(props, "effect_warning", obs_module_text(effectWarningKey.UTF8String), + OBS_TEXT_INFO); + obs_property_text_set_info_type(property, OBS_TEXT_INFO_WARNING); + } + } + + property = obs_properties_add_bool(props, "enable_downmix", obs_module_text("CoreAudio.Downmix")); + obs_property_set_modified_callback2(property, coreaudio_downmix_changed, ca); + + if (ca != NULL && ca->au_initialized) { + uint32_t channels = get_audio_channels(ca->speakers); + ensure_output_channels_visible(props, ca, channels); + + if (ca->enable_downmix) { + hide_all_output_channels(props); + } + } + + device_list_free(&devices); + return props; +} + +static obs_properties_t *coreaudio_input_properties(void *data) +{ + return coreaudio_properties(true, data); +} + +static obs_properties_t *coreaudio_output_properties(void *data) +{ + return coreaudio_properties(false, data); +} + +struct obs_source_info coreaudio_input_capture_info = { + .id = "coreaudio_input_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE, + .get_name = coreaudio_input_getname, + .create = coreaudio_create_input_capture, + .destroy = coreaudio_destroy, + .update = coreaudio_update, + .get_defaults = coreaudio_defaults, + .get_properties = coreaudio_input_properties, + .icon_type = OBS_ICON_TYPE_AUDIO_INPUT, +}; + +struct obs_source_info coreaudio_output_capture_info = { + .id = "coreaudio_output_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO | OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_DO_NOT_SELF_MONITOR, + .get_name = coreaudio_output_getname, + .create = coreaudio_create_output_capture, + .destroy = coreaudio_destroy, + .update = coreaudio_update, + .get_defaults = coreaudio_defaults, + .get_properties = coreaudio_output_properties, + .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT, +};