From 4b28631a957765528c51f3bdcc54e554357514af Mon Sep 17 00:00:00 2001 From: derrod Date: Sun, 27 Aug 2023 21:57:50 +0200 Subject: [PATCH] win-capture: Add audio capture option to window/game capture --- plugins/win-capture/CMakeLists.txt | 2 + plugins/win-capture/audio-helpers.c | 136 ++++++++++++++++++++++ plugins/win-capture/audio-helpers.h | 28 +++++ plugins/win-capture/cmake/legacy.cmake | 2 + plugins/win-capture/data/locale/en-US.ini | 3 + plugins/win-capture/game-capture.c | 46 +++++++- plugins/win-capture/window-capture.c | 39 ++++++- 7 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 plugins/win-capture/audio-helpers.c create mode 100644 plugins/win-capture/audio-helpers.h diff --git a/plugins/win-capture/CMakeLists.txt b/plugins/win-capture/CMakeLists.txt index d0b077e602405a..336aebb92f2085 100644 --- a/plugins/win-capture/CMakeLists.txt +++ b/plugins/win-capture/CMakeLists.txt @@ -44,6 +44,8 @@ target_sources( PRIVATE # cmake-format: sortable app-helpers.c app-helpers.h + audio-helpers.c + audio-helpers.h compat-format-ver.h compat-helpers.c compat-helpers.h diff --git a/plugins/win-capture/audio-helpers.c b/plugins/win-capture/audio-helpers.c new file mode 100644 index 00000000000000..276d96971b9bd0 --- /dev/null +++ b/plugins/win-capture/audio-helpers.c @@ -0,0 +1,136 @@ +#include "audio-helpers.h" + +#include + +static inline bool settings_changed(obs_data_t *old_settings, + obs_data_t *new_settings) +{ + const char *old_window = obs_data_get_string(old_settings, "window"); + const char *new_window = obs_data_get_string(new_settings, "window"); + + enum window_priority old_priority = + obs_data_get_int(old_settings, "priority"); + enum window_priority new_priority = + obs_data_get_int(new_settings, "priority"); + + // Changes to priority only matter if a window is set + return (old_priority != new_priority && *new_window) || + strcmp(old_window, new_window) != 0; +} + +static inline void reroute_wasapi_source(obs_source_t *wasapi, + obs_source_t *target) +{ + proc_handler_t *ph = obs_source_get_proc_handler(wasapi); + calldata_t *cd = calldata_create(); + calldata_set_ptr(cd, "target", target); + proc_handler_call(ph, "reroute_audio", cd); + calldata_free(cd); +} + +void setup_audio_source(obs_source_t *parent, obs_source_t **child, + const char *window, bool enabled, + enum window_priority priority) +{ + if (enabled) { + obs_data_t *wasapi_settings = NULL; + + if (window) { + wasapi_settings = obs_data_create(); + obs_data_set_string(wasapi_settings, "window", window); + obs_data_set_int(wasapi_settings, "priority", priority); + } + + if (!*child) { + struct dstr name = {0}; + dstr_printf(&name, "%s (%s)", + obs_source_get_name(parent), + TEXT_CAPTURE_AUDIO_SUFFIX); + + *child = obs_source_create_private( + AUDIO_SOURCE_TYPE, name.array, wasapi_settings); + + // Ensure child gets activated/deactivated properly + obs_source_add_active_child(parent, *child); + // Reroute audio to come from window/game capture source + reroute_wasapi_source(*child, parent); + // Show source in mixer + obs_source_set_audio_active(parent, true); + + dstr_free(&name); + } else if (wasapi_settings) { + obs_data_t *old_settings = + obs_source_get_settings(*child); + // Only bother updating if settings changed + if (settings_changed(old_settings, wasapi_settings)) + obs_source_update(*child, wasapi_settings); + + obs_data_release(old_settings); + } + + obs_data_release(wasapi_settings); + } else { + obs_source_set_audio_active(parent, false); + + if (*child) { + reroute_wasapi_source(*child, NULL); + obs_source_remove_active_child(parent, *child); + obs_source_release(*child); + *child = NULL; + } + } +} + +static inline void encode_dstr(struct dstr *str) +{ + dstr_replace(str, "#", "#22"); + dstr_replace(str, ":", "#3A"); +} + +void reconfigure_audio_source(obs_source_t *source, HWND window) +{ + struct dstr title = {0}; + struct dstr class = {0}; + struct dstr exe = {0}; + struct dstr encoded = {0}; + + ms_get_window_title(&title, window); + ms_get_window_class(&class, window); + ms_get_window_exe(&exe, window); + + encode_dstr(&title); + encode_dstr(&class); + encode_dstr(&exe); + + dstr_cat_dstr(&encoded, &title); + dstr_cat(&encoded, ":"); + dstr_cat_dstr(&encoded, &class); + dstr_cat(&encoded, ":"); + dstr_cat_dstr(&encoded, &exe); + + obs_data_t *audio_settings = obs_data_create(); + obs_data_set_string(audio_settings, "window", encoded.array); + obs_data_set_int(audio_settings, "priority", WINDOW_PRIORITY_CLASS); + + obs_source_update(source, audio_settings); + + obs_data_release(audio_settings); + dstr_free(&encoded); + dstr_free(&title); + dstr_free(&class); + dstr_free(&exe); +} + +void rename_audio_source(void *param, calldata_t *data) +{ + obs_source_t *src = *(obs_source_t **)param; + if (!src) + return; + + struct dstr name = {0}; + dstr_printf(&name, "%s (%s)", calldata_string(data, "new_name"), + TEXT_CAPTURE_AUDIO_SUFFIX); + + obs_source_set_name(src, name.array); + dstr_free(&name); +} diff --git a/plugins/win-capture/audio-helpers.h b/plugins/win-capture/audio-helpers.h new file mode 100644 index 00000000000000..e3e7e5358b386e --- /dev/null +++ b/plugins/win-capture/audio-helpers.h @@ -0,0 +1,28 @@ +#pragma once + +#include "obs-module.h" +#include + +#include "windows.h" + +#define SETTING_CAPTURE_AUDIO "capture_audio" +#define TEXT_CAPTURE_AUDIO obs_module_text("CaptureAudio") +#define TEXT_CAPTURE_AUDIO_TT obs_module_text("CaptureAudio.TT") +#define TEXT_CAPTURE_AUDIO_SUFFIX obs_module_text("AudioSuffix") +#define AUDIO_SOURCE_TYPE "wasapi_process_output_capture" + +void setup_audio_source(obs_source_t *parent, obs_source_t **child, + const char *window, bool enabled, + enum window_priority priority); +void reconfigure_audio_source(obs_source_t *source, HWND window); +void rename_audio_source(void *param, calldata_t *data); + +static bool audio_capture_available(void) +{ + return obs_get_latest_input_type_id(AUDIO_SOURCE_TYPE) != NULL; +} + +static void destroy_audio_source(obs_source_t *parent, obs_source_t **child) +{ + setup_audio_source(parent, child, NULL, false, 0); +} diff --git a/plugins/win-capture/cmake/legacy.cmake b/plugins/win-capture/cmake/legacy.cmake index adb63a7798b0fd..1b488a55b5afa3 100644 --- a/plugins/win-capture/cmake/legacy.cmake +++ b/plugins/win-capture/cmake/legacy.cmake @@ -20,6 +20,8 @@ target_sources( PRIVATE plugin-main.c app-helpers.c app-helpers.h + audio-helpers.c + audio-helpers.h cursor-capture.c cursor-capture.h dc-capture.c diff --git a/plugins/win-capture/data/locale/en-US.ini b/plugins/win-capture/data/locale/en-US.ini index 0a6c627bd255d1..e489b04d2c07e9 100644 --- a/plugins/win-capture/data/locale/en-US.ini +++ b/plugins/win-capture/data/locale/en-US.ini @@ -38,6 +38,9 @@ GameCapture.Rgb10a2Space="RGB10A2 Color Space" GameCapture.Rgb10a2Space.Srgb="sRGB" GameCapture.Rgb10a2Space.2100PQ="Rec. 2100 (PQ)" Mode="Mode" +CaptureAudio="Capture Audio (BETA)" +CaptureAudio.TT="When enabled, creates an \"Application Audio Capture\" source that automatically updates to the currently captured window/application. Note that if Desktop Audio is configured, this could result in doubled audio." +AudioSuffix="Audio" # Generic compatibility messages Compatibility.GameCapture.Admin="%name% may require OBS to be run as admin to use Game Capture." diff --git a/plugins/win-capture/game-capture.c b/plugins/win-capture/game-capture.c index 076cdad04ede7b..de7d66d39b2df0 100644 --- a/plugins/win-capture/game-capture.c +++ b/plugins/win-capture/game-capture.c @@ -17,6 +17,7 @@ #include "graphics-hook-ver.h" #include "cursor-capture.h" #include "app-helpers.h" +#include "audio-helpers.h" #include "nt-stuff.h" #define do_log(level, format, ...) \ @@ -117,6 +118,7 @@ struct game_capture_config { bool anticheat_hook; enum hook_rate hook_rate; bool is_10a2_2100pq; + bool capture_audio; }; typedef DPI_AWARENESS_CONTEXT(WINAPI *PFN_SetThreadDpiAwarenessContext)( @@ -126,6 +128,7 @@ typedef DPI_AWARENESS_CONTEXT(WINAPI *PFN_GetWindowDpiAwarenessContext)(HWND); struct game_capture { obs_source_t *source; + obs_source_t *audio_source; struct cursor_data cursor_data; HANDLE injector_process; @@ -398,6 +401,13 @@ static void game_capture_destroy(void *data) struct game_capture *gc = data; stop_capture(gc); + if (gc->audio_source) + destroy_audio_source(gc->source, &gc->audio_source); + + signal_handler_t *sh = obs_source_get_signal_handler(gc->source); + signal_handler_disconnect(sh, "rename", rename_audio_source, + &gc->audio_source); + if (gc->hotkey_pair) obs_hotkey_pair_unregister(gc->hotkey_pair); @@ -457,6 +467,7 @@ static inline void get_config(struct game_capture_config *cfg, cfg->is_10a2_2100pq = strcmp(obs_data_get_string(settings, SETTING_RGBA10A2_SPACE), "2100pq") == 0; + cfg->capture_audio = obs_data_get_bool(settings, SETTING_CAPTURE_AUDIO); } static inline int s_cmp(const char *str1, const char *str2) @@ -592,6 +603,11 @@ static void game_capture_update(void *data, obs_data_t *settings) } else { gc->initial_config = false; } + + /* Linked audio capture source stuff */ + setup_audio_source(gc->source, &gc->audio_source, + cfg.mode == CAPTURE_MODE_WINDOW ? window : NULL, + cfg.capture_audio, cfg.priority); } extern void wait_for_hook_initialization(void); @@ -651,6 +667,9 @@ static void *game_capture_create(obs_data_t *settings, obs_source_t *source) "void get_hooked(out bool hooked, out string title, out string class, out string executable)", game_capture_get_hooked, gc); + signal_handler_connect(sh, "rename", rename_audio_source, + &gc->audio_source); + game_capture_update(gc, settings); return gc; } @@ -1910,6 +1929,13 @@ static void game_capture_tick(void *data, float seconds) signal_handler_signal(sh, "hooked", &data); calldata_free(&data); + + // Update audio capture settings if not in window mode + if (gc->audio_source && + gc->config.mode != CAPTURE_MODE_WINDOW) { + reconfigure_audio_source(gc->audio_source, + gc->window); + } } if (result != CAPTURE_RETRY && !gc->capturing) { gc->retry_interval = @@ -2442,6 +2468,12 @@ static obs_properties_t *game_capture_properties(void *data) OBS_TEXT_INFO); obs_property_set_enabled(p, false); + if (audio_capture_available()) { + p = obs_properties_add_bool(ppts, SETTING_CAPTURE_AUDIO, + TEXT_CAPTURE_AUDIO); + obs_property_set_long_description(p, TEXT_CAPTURE_AUDIO_TT); + } + obs_properties_add_bool(ppts, SETTING_COMPATIBILITY, TEXT_SLI_COMPATIBILITY); @@ -2516,11 +2548,20 @@ game_capture_get_color_space(void *data, size_t count, return space; } +static void game_capture_enum(void *data, obs_source_enum_proc_t cb, + void *param) +{ + struct game_capture *gc = data; + if (gc->audio_source) + cb(gc->source, gc->audio_source, param); +} + struct obs_source_info game_capture_info = { .id = "game_capture", .type = OBS_SOURCE_TYPE_INPUT, - .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | - OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_SRGB, + .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_AUDIO | + OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_DO_NOT_DUPLICATE | + OBS_SOURCE_SRGB, .get_name = game_capture_name, .create = game_capture_create, .destroy = game_capture_destroy, @@ -2528,6 +2569,7 @@ struct obs_source_info game_capture_info = { .get_height = game_capture_height, .get_defaults = game_capture_defaults, .get_properties = game_capture_properties, + .enum_active_sources = game_capture_enum, .update = game_capture_update, .video_tick = game_capture_tick, .video_render = game_capture_render, diff --git a/plugins/win-capture/window-capture.c b/plugins/win-capture/window-capture.c index 54e7f9bd85cdb3..7166e0b59b14ef 100644 --- a/plugins/win-capture/window-capture.c +++ b/plugins/win-capture/window-capture.c @@ -2,7 +2,9 @@ #include #include #include + #include "dc-capture.h" +#include "audio-helpers.h" #include "compat-helpers.h" #ifdef OBS_LEGACY #include "../../libobs/util/platform.h" @@ -80,6 +82,7 @@ typedef DPI_AWARENESS_CONTEXT(WINAPI *PFN_GetWindowDpiAwarenessContext)(HWND); struct window_capture { obs_source_t *source; + obs_source_t *audio_source; pthread_mutex_t update_mutex; @@ -93,6 +96,7 @@ struct window_capture { bool client_area; bool force_sdr; bool hooked; + bool capture_audio; struct dc_capture capture; @@ -226,10 +230,14 @@ static void update_settings(struct window_capture *wc, obs_data_t *s) wc->method = choose_method(method, wgc_supported, wc->class); wc->priority = (enum window_priority)priority; wc->cursor = obs_data_get_bool(s, "cursor"); + wc->capture_audio = obs_data_get_bool(s, "capture_audio"); wc->force_sdr = obs_data_get_bool(s, "force_sdr"); wc->compatibility = obs_data_get_bool(s, "compatibility"); wc->client_area = obs_data_get_bool(s, "client_area"); + setup_audio_source(wc->source, &wc->audio_source, window, + wc->capture_audio, wc->priority); + pthread_mutex_unlock(&wc->update_mutex); } @@ -360,6 +368,9 @@ static void *wc_create(obs_data_t *settings, obs_source_t *source) "void get_hooked(out bool hooked, out string title, out string class, out string executable)", wc_get_hooked, wc); + signal_handler_connect(sh, "rename", rename_audio_source, + &wc->audio_source); + update_settings(wc, settings); log_settings(wc, settings); return wc; @@ -391,7 +402,15 @@ static void wc_actual_destroy(void *data) static void wc_destroy(void *data) { - obs_queue_task(OBS_TASK_GRAPHICS, wc_actual_destroy, data, false); + struct window_capture *wc = data; + if (wc->audio_source) + destroy_audio_source(wc->source, &wc->audio_source); + + signal_handler_t *sh = obs_source_get_signal_handler(wc->source); + signal_handler_disconnect(sh, "rename", rename_audio_source, + &wc->audio_source); + + obs_queue_task(OBS_TASK_GRAPHICS, wc_actual_destroy, wc, false); } static void force_reset(struct window_capture *wc) @@ -567,6 +586,12 @@ static obs_properties_t *wc_properties(void *data) p = obs_properties_add_text(ppts, "compat_info", NULL, OBS_TEXT_INFO); obs_property_set_enabled(p, false); + if (audio_capture_available()) { + p = obs_properties_add_bool(ppts, "capture_audio", + TEXT_CAPTURE_AUDIO); + obs_property_set_long_description(p, TEXT_CAPTURE_AUDIO_TT); + } + obs_properties_add_bool(ppts, "cursor", TEXT_CAPTURE_CURSOR); obs_properties_add_bool(ppts, "compatibility", TEXT_COMPATIBILITY); @@ -875,11 +900,18 @@ wc_get_color_space(void *data, size_t count, return space; } +static void wc_child_enum(void *data, obs_source_enum_proc_t cb, void *param) +{ + struct window_capture *wc = data; + if (wc->audio_source) + cb(wc->source, wc->audio_source, param); +} + struct obs_source_info window_capture_info = { .id = "window_capture", .type = OBS_SOURCE_TYPE_INPUT, - .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW | - OBS_SOURCE_SRGB, + .output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_AUDIO | + OBS_SOURCE_CUSTOM_DRAW | OBS_SOURCE_SRGB, .get_name = wc_getname, .create = wc_create, .destroy = wc_destroy, @@ -891,6 +923,7 @@ struct obs_source_info window_capture_info = { .get_height = wc_height, .get_defaults = wc_defaults, .get_properties = wc_properties, + .enum_active_sources = wc_child_enum, .icon_type = OBS_ICON_TYPE_WINDOW_CAPTURE, .video_get_color_space = wc_get_color_space, };