diff --git a/libobs/media-io/audio-resampler-ffmpeg.c b/libobs/media-io/audio-resampler-ffmpeg.c index eb5be4f7435e2e..67235b8241c1b3 100644 --- a/libobs/media-io/audio-resampler-ffmpeg.c +++ b/libobs/media-io/audio-resampler-ffmpeg.c @@ -216,3 +216,27 @@ bool audio_resampler_resample(audio_resampler_t *rs, uint8_t *output[], uint32_t *out_frames = (uint32_t)ret; return true; } + +// Allows passing of a matrix for channel re-ordering and custom weights. +bool audio_resampler_set_matrix(audio_resampler_t *rs, double *matrix, int stride) +{ + if (!rs) { + return false; + } + + swr_close(rs->context); + + if (swr_set_matrix(rs->context, matrix, stride) < 0) { + blog(LOG_DEBUG, "swr_set_matrix failed\n"); + audio_resampler_destroy(rs); + return false; + } + + int errcode = swr_init(rs->context); + if (errcode != 0) { + blog(LOG_ERROR, "swr_init failed: error code %d", errcode); + audio_resampler_destroy(rs); + return false; + } + return true; +} diff --git a/libobs/media-io/audio-resampler.h b/libobs/media-io/audio-resampler.h index ca1e7b08d58000..25f9b2872513d7 100644 --- a/libobs/media-io/audio-resampler.h +++ b/libobs/media-io/audio-resampler.h @@ -39,6 +39,8 @@ EXPORT void audio_resampler_destroy(audio_resampler_t *resampler); EXPORT bool audio_resampler_resample(audio_resampler_t *resampler, uint8_t *output[], uint32_t *out_frames, uint64_t *ts_offset, const uint8_t *const input[], uint32_t in_frames); +EXPORT bool audio_resampler_set_matrix(audio_resampler_t *rs, double *matrix, int stride); + #ifdef __cplusplus } #endif diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index 40c62a4e3604fc..516c27d9b42a7f 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -8,6 +8,7 @@ CoreAudio.Channel.Unnamed="Unnamed" CoreAudio.Channel.Device="Device Channel" CoreAudio.None="None" CoreAudio.Downmix="Enable Downmixing" +CoreAudio.Downmix.Hint="By default OBS will automatically upmix or downmix to the number of channels configured in settings. Enabling this setting will allow you to manually select which channels of the device to map to the input in OBS." ApplicationCapture="Application Capture" ApplicationAudioCapture="Application Audio Capture" DesktopAudioCapture="Desktop Audio Capture" diff --git a/plugins/mac-capture/mac-audio.c b/plugins/mac-capture/mac-audio.c index 8d9dca9972fbe7..c0c09f6d8840be 100644 --- a/plugins/mac-capture/mac-audio.c +++ b/plugins/mac-capture/mac-audio.c @@ -1015,6 +1015,7 @@ 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_long_description(property, obs_module_text("CoreAudio.Downmix.Hint")); obs_property_set_modified_callback2(property, coreaudio_downmix_changed, ca); if (ca != NULL && ca->au_initialized) { diff --git a/plugins/win-wasapi/data/locale/en-US.ini b/plugins/win-wasapi/data/locale/en-US.ini index 58531fc864eb88..fa3ba050958fdc 100644 --- a/plugins/win-wasapi/data/locale/en-US.ini +++ b/plugins/win-wasapi/data/locale/en-US.ini @@ -9,3 +9,14 @@ Priority="Window Match Priority" Priority.Title="Window title must match" Priority.Class="Match title, otherwise find window of same type" Priority.Exe="Match title, otherwise find window of same executable" +Routing="Enable Channel Routing" +Routing.Hint="By default OBS will automatically upmix or downmix to the number of channels configured in settings.\nEnabling this setting will allow you to manually select which channels of the device to map to the input in OBS." +OBS_channel.0="OBS Channel 1" +OBS_channel.1="OBS Channel 2" +OBS_channel.2="OBS Channel 3" +OBS_channel.3="OBS Channel 4" +OBS_channel.4="OBS Channel 5" +OBS_channel.5="OBS Channel 6" +OBS_channel.6="OBS Channel 7" +OBS_channel.7="OBS Channel 8" +None="None" diff --git a/plugins/win-wasapi/enum-wasapi.cpp b/plugins/win-wasapi/enum-wasapi.cpp index 697834390bf336..9329be8d44f748 100644 --- a/plugins/win-wasapi/enum-wasapi.cpp +++ b/plugins/win-wasapi/enum-wasapi.cpp @@ -1,6 +1,7 @@ #include "enum-wasapi.hpp" #include +#include #include #include #include @@ -34,6 +35,26 @@ string GetDeviceName(IMMDevice *device) return device_name; } +std::string GetWASAPIDefaultDeviceName(bool input) +{ + ComPtr enumerator; + ComPtr device; + + HRESULT res = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), + (void **)enumerator.Assign()); + if (FAILED(res)) { + return ""; + } + + res = enumerator->GetDefaultAudioEndpoint(input ? eCapture : eRender, input ? eCommunications : eConsole, + device.Assign()); + if (FAILED(res)) { + return ""; + } + + return GetDeviceName(device); +} + static void GetWASAPIAudioDevices_(vector &devices, bool input) { ComPtr enumerator; @@ -90,3 +111,45 @@ void GetWASAPIAudioDevices(vector &devices, bool input) blog(LOG_WARNING, "[GetWASAPIAudioDevices] %s: %lX", error.str, error.hr); } } + +int GetWASAPIDeviceInputChannels(const char *device_id) +{ + ComPtr enumerator; + ComPtr device; + ComPtr client; + CoTaskMemPtr wfex; + + HRESULT res = + CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(enumerator.Assign())); + if (FAILED(res)) { + return 0; + } + + if (strcmp(device_id, "default") == 0) { + res = enumerator->GetDefaultAudioEndpoint(eCapture, eCommunications, device.Assign()); + } else { + wchar_t *w_id = nullptr; + os_utf8_to_wcs_ptr(device_id, strlen(device_id), &w_id); + if (!w_id) { + return 0; + } + res = enumerator->GetDevice(w_id, device.Assign()); + bfree(w_id); + } + + if (FAILED(res) || !device) { + return 0; + } + + res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **)client.Assign()); + if (FAILED(res)) { + return 0; + } + + res = client->GetMixFormat(&wfex); + if (FAILED(res) || !wfex) { + return 0; + } + + return wfex->nChannels; +} diff --git a/plugins/win-wasapi/enum-wasapi.hpp b/plugins/win-wasapi/enum-wasapi.hpp index 57761b9d352d7d..37b939aea73497 100644 --- a/plugins/win-wasapi/enum-wasapi.hpp +++ b/plugins/win-wasapi/enum-wasapi.hpp @@ -38,4 +38,6 @@ struct AudioDeviceInfo { }; std::string GetDeviceName(IMMDevice *device); +std::string GetWASAPIDefaultDeviceName(bool input); void GetWASAPIAudioDevices(std::vector &devices, bool input); +int GetWASAPIDeviceInputChannels(const char *device_id); diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 0cea4758559b2f..2d08a694adda90 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -3,25 +3,26 @@ #include #include +#include #include #include -#include +#include +#include +#include #include +#include #include -#include #include #include -#include -#include - -#include -#include +#include #include #include -#include #include +#include +#include + using namespace std; #define OPT_DEVICE_ID "device_id" @@ -149,7 +150,7 @@ class WASAPISource { obs_weak_source_t *reroute_target = nullptr; wstring default_id; string device_id; - string device_name; + WinModule mmdevapi_module; PFN_ActivateAudioInterfaceAsync activate_audio_interface_async = NULL; PFN_RtwqUnlockWorkQueue rtwq_unlock_work_queue = NULL; @@ -227,6 +228,7 @@ class WASAPISource { speaker_layout speakers; audio_format format; uint32_t sampleRate; + audio_resampler_t *resampler; vector silence; @@ -240,11 +242,11 @@ class WASAPISource { static ComPtr InitDevice(IMMDeviceEnumerator *enumerator, bool isDefaultDevice, SourceType type, const string device_id); - static ComPtr InitClient(IMMDevice *device, SourceType type, DWORD process_id, - PFN_ActivateAudioInterfaceAsync activate_audio_interface_async, - speaker_layout &speakers, audio_format &format, uint32_t &sampleRate); - static void InitFormat(const WAVEFORMATEX *wfex, enum speaker_layout &speakers, enum audio_format &format, - uint32_t &sampleRate); + ComPtr InitClient(IMMDevice *device, SourceType type, DWORD process_id, + PFN_ActivateAudioInterfaceAsync activate_audio_interface_async, + speaker_layout &speakers, audio_format &format, uint32_t &sampleRate); + void InitFormat(const WAVEFORMATEX *wfex, enum speaker_layout &speakers, enum audio_format &format, + uint32_t &sampleRate); static void ClearBuffer(IMMDevice *device); static ComPtr InitCapture(IAudioClient *client, HANDLE receiveSignal); void Initialize(); @@ -259,12 +261,16 @@ class WASAPISource { string window_class; string title; string executable; + bool enableChannelRouting; + int mixChannels[MAX_AUDIO_CHANNELS]; }; UpdateParams BuildUpdateParams(obs_data_t *settings); void UpdateSettings(UpdateParams &¶ms); void LogSettings(); + void UpdateResamplerMatrix(); + public: WASAPISource(obs_data_t *settings, obs_source_t *source_, SourceType type); ~WASAPISource(); @@ -289,6 +295,15 @@ class WASAPISource { obs_weak_source_release(reroute_target); reroute_target = obs_source_get_weak_source(target); } + + string device_name; + std::atomic enableChannelRouting; + std::atomic deviceUpdated; + std::atomic resamplerMatrixNeedsUpdate; + int inputChannels; + int mixChannels[MAX_AUDIO_CHANNELS]; + size_t audioCapacity; + const uint8_t *resamplerData[MAX_AUDIO_CHANNELS]; }; WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, SourceType type) @@ -304,6 +319,14 @@ WASAPISource::WASAPISource(obs_data_t *settings, obs_source_t *source_, SourceTy (PFN_ActivateAudioInterfaceAsync)GetProcAddress(mmdevapi_module, "ActivateAudioInterfaceAsync"); } + resampler = nullptr; + deviceUpdated.store(false, std::memory_order_relaxed); + resamplerData[0] = nullptr; + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + mixChannels[i] = -1; + } + resamplerMatrixNeedsUpdate.store(true, std::memory_order_release); + UpdateSettings(BuildUpdateParams(settings)); LogSettings(); @@ -469,6 +492,14 @@ WASAPISource::~WASAPISource() obs_source_audio_output_capture_device_changed(source, NULL); Stop(); + + if (sourceType == SourceType::Input) { + if (resampler != nullptr) { + audio_resampler_destroy(resampler); + resampler = nullptr; + } + bfree((void *)resamplerData[0]); + } } WASAPISource::UpdateParams WASAPISource::BuildUpdateParams(obs_data_t *settings) @@ -481,6 +512,30 @@ WASAPISource::UpdateParams WASAPISource::BuildUpdateParams(obs_data_t *settings) params.window_class.clear(); params.title.clear(); params.executable.clear(); + + if (sourceType == SourceType::Input) { + params.enableChannelRouting = obs_data_get_bool(settings, "enable_routing"); + bool routingUpdated = params.enableChannelRouting != + enableChannelRouting.load(std::memory_order_relaxed); + + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + std::string channel_str = "OBS_channel." + std::to_string(i); + params.mixChannels[i] = (int)obs_data_get_int(settings, channel_str.c_str()); + } + + deviceUpdated.store(params.device_id != device_id && device_id != "", std::memory_order_relaxed); + + if (routingUpdated || deviceUpdated.load(std::memory_order_relaxed)) { + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + std::string channel_str = "OBS_channel." + std::to_string(i); + obs_data_set_int(settings, channel_str.c_str(), -1); + params.mixChannels[i] = -1; + mixChannels[i] = -1; + } + resamplerMatrixNeedsUpdate.store(true, std::memory_order_release); + } + } + if (sourceType != SourceType::Input) { const char *const window = obs_data_get_string(settings, OPT_WINDOW); char *window_class = nullptr; @@ -517,6 +572,20 @@ void WASAPISource::UpdateSettings(UpdateParams &¶ms) window_class = std::move(params.window_class); title = std::move(params.title); executable = std::move(params.executable); + if (sourceType == SourceType::Input) { + enableChannelRouting.store(std::move(params.enableChannelRouting), std::memory_order_relaxed); + bool changed = false; + for (int i = 0; i < MAX_AUDIO_CHANNELS; i++) { + if (mixChannels[i] != params.mixChannels[i]) { + changed = true; + } + + mixChannels[i] = std::move(params.mixChannels[i]); + } + if (changed) { + resamplerMatrixNeedsUpdate.store(true, std::memory_order_release); + } + } } void WASAPISource::LogSettings() @@ -766,6 +835,10 @@ void WASAPISource::ClearBuffer(IMMDevice *device) static speaker_layout ConvertSpeakerLayout(DWORD layout, WORD channels) { switch (layout) { + case KSAUDIO_SPEAKER_MONO: + return SPEAKERS_MONO; + case KSAUDIO_SPEAKER_STEREO: + return SPEAKERS_STEREO; case KSAUDIO_SPEAKER_2POINT1: return SPEAKERS_2POINT1; case KSAUDIO_SPEAKER_SURROUND: @@ -776,9 +849,9 @@ static speaker_layout ConvertSpeakerLayout(DWORD layout, WORD channels) return SPEAKERS_5POINT1; case KSAUDIO_SPEAKER_7POINT1_SURROUND: return SPEAKERS_7POINT1; + default: + return SPEAKERS_UNKNOWN; } - - return (speaker_layout)channels; } void WASAPISource::InitFormat(const WAVEFORMATEX *wfex, enum speaker_layout &speakers, enum audio_format &format, @@ -793,8 +866,16 @@ void WASAPISource::InitFormat(const WAVEFORMATEX *wfex, enum speaker_layout &spe /* WASAPI is always float */ speakers = ConvertSpeakerLayout(layout, wfex->nChannels); + + if (!speakers && sourceType == SourceType::Input) { + blog(LOG_INFO, "WASAPI: Input Device has a non-supported speaker layout." + " Consider disabling automatic downmix to do a manual channel select."); + speakers = (speaker_layout)wfex->nChannels; + } + format = AUDIO_FORMAT_FLOAT; sampleRate = wfex->nSamplesPerSec; + inputChannels = sourceType == SourceType::Input ? wfex->nChannels : 0; } ComPtr WASAPISource::InitCapture(IAudioClient *client, HANDLE receiveSignal) @@ -815,6 +896,36 @@ ComPtr WASAPISource::InitCapture(IAudioClient *client, HAND return capture; } +void WASAPISource::UpdateResamplerMatrix() +{ + if (!resampler) { + return; + } + + const int out_ch = (int)audio_output_get_channels(obs_get_audio()); + const int in_ch = inputChannels; + + if (!in_ch) { + return; + } + + std::vector matrix(out_ch * in_ch, 0.0); + const int stride = in_ch; + + for (int out = 0; out < out_ch; ++out) { + const int in = mixChannels[out]; + if (in >= 0 && in < in_ch) { + matrix[out * stride + in] = 1.0; + } + } + + if (!audio_resampler_set_matrix(resampler, matrix.data(), stride)) { + blog(LOG_WARNING, "WASAPI: failed to set resampler matrix (in=%d, out=%d)", in_ch, out_ch); + audio_resampler_destroy(resampler); + resampler = nullptr; + } +} + void WASAPISource::Initialize() { ComPtr device; @@ -850,6 +961,32 @@ void WASAPISource::Initialize() client = std::move(temp_client); capture = std::move(temp_capture); + deviceUpdated.store(false, std::memory_order_relaxed); + + if (resampler != nullptr) { + audio_resampler_destroy(resampler); + resampler = nullptr; + bfree((void *)resamplerData[0]); + resamplerData[0] = nullptr; + } + + enum speaker_layout dst_speakers = (enum speaker_layout)audio_output_get_channels(obs_get_audio()); + size_t size = get_audio_size(format, dst_speakers, 1); + audioCapacity = 1; + resamplerData[0] = (const uint8_t *)bmalloc(size); + + struct resample_info src, dst; + + src.samples_per_sec = sampleRate; + src.format = format; + src.speakers = (enum speaker_layout)inputChannels; + + dst.samples_per_sec = sampleRate; + dst.format = format; + dst.speakers = (enum speaker_layout)audio_output_get_channels(obs_get_audio()); + + resampler = audio_resampler_create(&dst, &src); + if (rtwq_supported) { HRESULT hr = rtwq_put_waiting_work_item(receiveSignal, 0, sampleReadyAsyncResult, nullptr); if (FAILED(hr)) { @@ -1013,8 +1150,32 @@ bool WASAPISource::ProcessCaptureData() obs_source_audio data = {}; data.data[0] = buffer; - data.frames = frames; data.speakers = speakers; + if (sourceType == SourceType::Input) { + if (resamplerMatrixNeedsUpdate.exchange(false, std::memory_order_acq_rel)) { + UpdateResamplerMatrix(); + } + + if (enableChannelRouting.load(std::memory_order_relaxed) && resampler) { + enum speaker_layout dst_speakers = + (enum speaker_layout)audio_output_get_channels(obs_get_audio()); + size_t size = get_audio_size(format, dst_speakers, frames); + uint8_t *output[8]; + uint32_t out_frames; + uint64_t ts_offset; + + if (audioCapacity < frames) { + resamplerData[0] = (const uint8_t *)brealloc((void *)resamplerData[0], size); + audioCapacity = frames; + } + data.speakers = dst_speakers; + audio_resampler_resample(resampler, (uint8_t **)output, &out_frames, &ts_offset, + (const uint8_t **)&buffer, (uint32_t)frames); + memcpy((void *)resamplerData[0], output[0], size); + data.data[0] = resamplerData[0]; + } + } + data.frames = frames; data.samples_per_sec = sampleRate; data.format = format; if (sourceType == SourceType::ProcessOutput) { @@ -1331,6 +1492,12 @@ static void GetWASAPIDefaultsInput(obs_data_t *settings) { obs_data_set_default_string(settings, OPT_DEVICE_ID, "default"); obs_data_set_default_bool(settings, OPT_USE_DEVICE_TIMING, false); + obs_data_set_default_bool(settings, "enable_routing", false); + + for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { + string channel_str = "OBS_channel." + std::to_string(i); + obs_data_set_default_int(settings, channel_str.c_str(), -1); + } } static void GetWASAPIDefaultsDeviceOutput(obs_data_t *settings) @@ -1461,10 +1628,96 @@ static bool UpdateWASAPIMethod(obs_properties_t *props, obs_property_t *, obs_da return true; } -static obs_properties_t *GetWASAPIPropertiesInput(void *) +static bool channel_update_cb(void *vptr, obs_properties_t *props, obs_property_t *chanlist, obs_data_t *settings) +{ + UNUSED_PARAMETER(props); + WASAPISource *data = (WASAPISource *)vptr; + if (data == NULL) { + return false; + } + + const char *device_id = obs_data_get_string(settings, OPT_DEVICE_ID); + std::string device_name; + if (strcmp(device_id, "default") == 0) { + device_name = GetWASAPIDefaultDeviceName(true); + } else { + std::vector devices; + GetWASAPIAudioDevices(devices, true); + for (int i = 0; i < devices.size(); i++) { + if (strcmp(device_id, devices[i].id.c_str()) == 0) { + device_name = devices[i].name; + break; + } + } + } + + int inputChannels = GetWASAPIDeviceInputChannels(device_id); + obs_property_list_clear(chanlist); + obs_property_list_add_int(chanlist, obs_module_text("None"), -1); + for (int j = 0; j < inputChannels; ++j) { + char label[256]; + snprintf(label, sizeof(label), "%s channel %d", device_name.c_str(), j + 1); + obs_property_list_add_int(chanlist, label, j); + } + + return true; +} + +bool reload_channel_props(void *vptr, obs_properties_t *props, obs_property_t *devlist, obs_data_t *settings, + bool update_settings) +{ + WASAPISource *data = (WASAPISource *)vptr; + if (data == NULL) { + return false; + } + + bool enable_routing = obs_data_get_bool(settings, "enable_routing"); + int obs_channels = (int)audio_output_get_channels(obs_get_audio()); + std::vector mix_channels(MAX_AUDIO_CHANNELS, nullptr); + + for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { + string channel_str = "OBS_channel." + std::to_string(i); + mix_channels[i] = obs_properties_get(props, channel_str.c_str()); + obs_property_set_visible(mix_channels[i], i < obs_channels && enable_routing); + if (update_settings) { + obs_data_set_int(settings, channel_str.c_str(), -1); + } + obs_property_set_modified_callback2(mix_channels[i], channel_update_cb, data); + channel_update_cb(data, props, mix_channels[i], settings); + } + return true; +} + +static bool device_update_cb(void *vptr, obs_properties_t *props, obs_property_t *devlist, obs_data_t *settings) +{ + WASAPISource *data = (WASAPISource *)vptr; + if (data == NULL) { + return false; + } + + return reload_channel_props(data, props, devlist, settings, + data->deviceUpdated.load(std::memory_order_relaxed)); +} + +static bool wasapi_enable_routing_changed(void *vptr, obs_properties_t *props, obs_property_t *devlist, + obs_data_t *settings) { + WASAPISource *data = (WASAPISource *)vptr; + if (data == NULL) { + return false; + } + + bool enable_routing = obs_data_get_bool(settings, "enable_routing"); + return reload_channel_props(data, props, devlist, settings, + data->enableChannelRouting.load(std::memory_order_relaxed) != enable_routing); +} + +static obs_properties_t *GetWASAPIPropertiesInput(void *vptr) +{ + WASAPISource *data = (WASAPISource *)vptr; obs_properties_t *props = obs_properties_create(); vector devices; + obs_property_t *property; obs_property_t *device_prop = obs_properties_add_list(props, OPT_DEVICE_ID, obs_module_text("Device"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); @@ -1479,8 +1732,34 @@ static obs_properties_t *GetWASAPIPropertiesInput(void *) obs_property_list_add_string(device_prop, device.name.c_str(), device.id.c_str()); } + if (data != nullptr) { + obs_property_set_modified_callback2(device_prop, device_update_cb, data); + } + obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); + if (data != nullptr) { + property = obs_properties_add_bool(props, "enable_routing", obs_module_text("Routing")); + obs_property_set_modified_callback2(property, wasapi_enable_routing_changed, data); + obs_property_set_long_description(property, obs_module_text("Routing.Hint")); + + int obs_channels = (int)audio_output_get_channels(obs_get_audio()); + for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { + string channel_str = "OBS_channel." + std::to_string(i); + property = obs_properties_add_list(props, channel_str.c_str(), + obs_module_text(channel_str.c_str()), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_set_visible(property, i < obs_channels && data->enableChannelRouting.load( + std::memory_order_relaxed)); + obs_property_list_add_int(property, "Mute", -1); + + for (int j = 0; j < data->inputChannels; ++j) { + string channel_device_str = data->device_name + " channel " + std::to_string(j); + obs_property_list_add_int(property, channel_device_str.c_str(), j); + } + } + } + return props; }