mac-capture: Switch AudioUnits to use canonical format on output bus

This commit is contained in:
PatTheMav 2023-10-25 00:19:36 +02:00 committed by Ryan Foster
parent 73e9561633
commit 5697b085da
2 changed files with 213 additions and 99 deletions

View file

@ -2,6 +2,12 @@ CoreAudio.InputCapture="Audio Input Capture"
CoreAudio.OutputCapture="Audio Output Capture"
CoreAudio.Device="Device"
CoreAudio.Device.Default="Default"
CoreAudio.Channel="Channel"
CoreAudio.Channel.Default="Default"
CoreAudio.Channel.Unnamed="Unnamed"
CoreAudio.Channel.Device="Device Channel"
CoreAudio.None="None"
CoreAudio.Downmix="Enable Downmixing"
ApplicationCapture="Application Capture"
ApplicationAudioCapture="Application Audio Capture"
DesktopAudioCapture="Desktop Audio Capture"

View file

@ -49,6 +49,7 @@ struct coreaudio_data {
uint32_t sample_rate;
enum audio_format format;
enum speaker_layout speakers;
bool enable_downmix;
pthread_t reconnect_thread;
os_event_t *exit_event;
@ -110,7 +111,7 @@ static bool find_device_id_by_uid(struct coreaudio_data *ca)
kCFStringEncodingUTF8);
if (ca->default_device) {
addr.mSelector = PROPERTY_DEFAULT_DEVICE;
addr.mSelector = kAudioHardwarePropertyDefaultInputDevice;
stat = AudioObjectGetPropertyData(kAudioObjectSystemObject,
&addr, qual_size, &qual,
&size, &ca->device_id);
@ -169,6 +170,27 @@ static inline bool enable_io(struct coreaudio_data *ca,
&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)
{
@ -192,11 +214,11 @@ static inline enum audio_format convert_ca_format(UInt32 format_flags,
return AUDIO_FORMAT_UNKNOWN;
}
static char *sanitise_device_name(char *name)
static char *sanitize_device_name(char *name)
{
const size_t max_len = 64;
size_t len = strlen(name);
char buf[max_len];
char buf[64];
size_t out_idx = 0;
for (size_t i = len > max_len ? len - max_len : 0; i < len; i++) {
@ -230,12 +252,18 @@ static char **coreaudio_get_channel_names(struct coreaudio_data *ca)
if (ca_success(stat, ca, "coreaudio_init_format",
"get channel names") &&
CFStringGetLength(cf_chan_name)) {
dstr_printf(
&name, "%d - %s", i + 1,
CFStringGetCStringPtr(cf_chan_name,
kCFStringEncodingUTF8));
char *channelName = cfstr_copy_cstr(
cf_chan_name, kCFStringEncodingUTF8);
dstr_printf(&name, "%s", channelName);
if (channelName) {
bfree(channelName);
}
} else {
dstr_printf(&name, "%d - Channel %d [unnamed]", i + 1,
dstr_printf(&name, "%s %d",
obs_module_text("CoreAudio.Channel.Device"),
i + 1);
}
channel_names[i] = bstrdup_n(name.array, name.len + 1);
@ -251,6 +279,7 @@ static char **coreaudio_get_channel_names(struct coreaudio_data *ca)
static bool coreaudio_init_format(struct coreaudio_data *ca)
{
AudioStreamBasicDescription desc;
AudioStreamBasicDescription inputDescription;
OSStatus stat;
UInt32 size;
struct obs_audio_info aoi;
@ -261,45 +290,62 @@ static bool coreaudio_init_format(struct coreaudio_data *ca)
ca->speakers = aoi.speakers;
uint32_t channels = get_audio_channels(ca->speakers);
size = sizeof(desc);
size = sizeof(inputDescription);
stat = get_property(ca->unit, kAudioUnitProperty_StreamFormat,
SCOPE_INPUT, BUS_INPUT, &desc, &size);
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 = desc.mChannelsPerFrame;
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);
// Always work out the format from the number of channels in the speaker layout
desc.mChannelsPerFrame = channels;
desc.mBytesPerFrame = desc.mChannelsPerFrame * desc.mBitsPerChannel / 8;
desc.mBytesPerPacket = desc.mFramesPerPacket * desc.mBytesPerFrame;
// 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;
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 = ",";
}
dstr_catf(&cm_str, "%d", ca->channel_map[i]);
sep = ",";
}
blog(LOG_INFO, "Channel map [%s]. Inputs available: %d", cm_str.array,
ca->available_channels);
dstr_free(&cm_str);
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_INPUT, BUS_INPUT, ca->channel_map,
sizeof(SInt32) * channels);
if (!ca_success(stat, ca, "coreaudio_init_format", "set channel map"))
return false;
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);
@ -328,55 +374,72 @@ static bool coreaudio_init_format(struct coreaudio_data *ca)
static bool coreaudio_init_buffer(struct coreaudio_data *ca)
{
UInt32 buf_size = 0;
UInt32 size = 0;
UInt32 frames = 0;
OSStatus stat;
UInt32 bufferSizeFrames;
UInt32 bufferSizeBytes;
UInt32 propertySize;
OSStatus err = noErr;
AudioObjectPropertyAddress addr = {
kAudioDevicePropertyStreamConfiguration,
kAudioDevicePropertyScopeInput,
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};
stat = AudioObjectGetPropertyDataSize(ca->device_id, &addr, 0, NULL,
&buf_size);
if (!ca_success(stat, ca, "coreaudio_init_buffer", "get list size"))
return false;
err = AudioObjectGetPropertyData(ca->device_id, &propertyAddress, 0,
NULL, &propertySize, &rate);
size = sizeof(frames);
stat = get_property(ca->unit, kAudioDevicePropertyBufferFrameSize,
SCOPE_GLOBAL, 0, &frames, &size);
if (!ca_success(stat, ca, "coreaudio_init_buffer", "get frame size"))
return false;
/* ---------------------- */
AudioBufferList *buf_list = bmalloc(buf_size);
stat = AudioObjectGetPropertyData(ca->device_id, &addr, 0, NULL,
&buf_size, buf_list);
if (!ca_success(stat, ca, "coreaudio_init_buffer", "allocate")) {
bfree(buf_list);
buf_list = NULL;
if (!ca_success(err, ca, "coreaudio_init_buffer",
"get input sample rate")) {
return false;
}
int channels = get_audio_channels(ca->speakers);
for (UInt32 i = 0; i < buf_list->mNumberBuffers; i++) {
// The returned buffer seems to ignore the channel mapping.
// This corrects it to take that into account -
// but this probably wouldn't work for planar formats.
// We are probably better off explicitly setting the format we want,
// and just taking sample rate from the device
uint chan_size = buf_list->mBuffers[i].mDataByteSize /
buf_list->mBuffers[i].mNumberChannels;
buf_list->mBuffers[i].mDataByteSize = chan_size * channels;
buf_list->mBuffers[i].mNumberChannels = channels;
size = buf_list->mBuffers[i].mDataByteSize;
buf_list->mBuffers[i].mData = bmalloc(size);
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 = buf_list;
ca->buf_list = inputBuffer;
return true;
}
@ -408,7 +471,7 @@ static OSStatus input_callback(void *data,
audio.data[i] = ca->buf_list->mBuffers[i].mData;
audio.frames = frames;
audio.speakers = ca->speakers;
audio.speakers = ca->buf_list->mNumberBuffers;
audio.format = ca->format;
audio.samples_per_sec = ca->sample_rate;
static double factor = 0.;
@ -802,7 +865,7 @@ static void coreaudio_set_channels(struct coreaudio_data *ca,
{
ca->channel_map = bzalloc(sizeof(SInt32) * MAX_AV_PLANES);
char *device_config_name = sanitise_device_name(ca->device_uid);
char *device_config_name = sanitize_device_name(ca->device_uid);
for (uint8_t i = 0; i < MAX_AV_PLANES; i++) {
char setting_name[128];
snprintf(setting_name, 128, "output-%s-%i", device_config_name,
@ -825,7 +888,12 @@ static void coreaudio_update(void *data, obs_data_t *settings)
bfree(ca->device_uid);
ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id"));
coreaudio_set_channels(ca, settings);
ca->enable_downmix = obs_data_get_bool(settings, "enable_downmix");
if (!ca->enable_downmix) {
coreaudio_set_channels(ca, settings);
}
coreaudio_try_init(ca);
}
@ -833,6 +901,7 @@ static void coreaudio_update(void *data, obs_data_t *settings)
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,
@ -852,7 +921,11 @@ static void *coreaudio_create(obs_data_t *settings, obs_source_t *source,
ca->device_uid = bstrdup(obs_data_get_string(settings, "device_id"));
ca->source = source;
ca->input = input;
coreaudio_set_channels(ca, settings);
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");
@ -874,34 +947,35 @@ static void *coreaudio_create_output_capture(obs_data_t *settings,
}
static void coreaudio_fill_combo_with_inputs(const struct coreaudio_data *ca,
uint32_t out_chan,
obs_property_t *input_combo)
obs_property_t *input_combo,
uint32_t output_channel)
{
bool hasMutedChannel = false;
obs_property_list_clear(input_combo);
bool defaulted = out_chan < ca->available_channels;
if (defaulted) {
struct dstr default_name;
dstr_init(&default_name);
dstr_printf(&default_name, "%s [Default]",
ca->channel_names[out_chan]);
obs_property_list_add_int(input_combo, default_name.array,
out_chan + 1);
dstr_free(&default_name);
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, "Mute [Default]", -1);
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 (!defaulted || input_chan != out_chan) {
if (input_chan != output_channel) {
obs_property_list_add_int(input_combo,
ca->channel_names[input_chan],
input_chan + 1);
}
}
if (defaulted) {
obs_property_list_add_int(input_combo, "Mute", -1);
if (!hasMutedChannel) {
obs_property_list_add_int(
input_combo, obs_module_text("CoreAudio.None"), -1);
}
}
@ -921,12 +995,13 @@ static void ensure_output_channel_prop(const struct coreaudio_data *ca,
} else {
struct dstr label;
dstr_init(&label);
dstr_printf(&label, "Output %i", out_chan + 1);
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, out_chan, input_combo);
coreaudio_fill_combo_with_inputs(ca, input_combo, out_chan);
}
dstr_free(&name);
}
@ -935,7 +1010,7 @@ static void ensure_output_channels_visible(obs_properties_t *props,
const struct coreaudio_data *ca,
uint32_t channels)
{
char *device_config_name = sanitise_device_name(ca->device_uid);
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);
@ -959,15 +1034,39 @@ static bool coreaudio_device_changed(void *data, obs_properties_t *props,
{
struct coreaudio_data *ca = data;
if (ca != NULL) {
uint32_t channels = get_audio_channels(ca->speakers);
hide_all_output_channels(props);
ensure_output_channels_visible(props, ca, channels);
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;
@ -996,9 +1095,18 @@ static obs_properties_t *coreaudio_properties(bool input, void *data)
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) {
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);