Browse Source

win-capture: Add audio capture option to window/game capture

derrod 2 years ago
parent
commit
4b28631a95

+ 2 - 0
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

+ 136 - 0
plugins/win-capture/audio-helpers.c

@@ -0,0 +1,136 @@
+#include "audio-helpers.h"
+
+#include <util/dstr.h>
+
+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);
+}

+ 28 - 0
plugins/win-capture/audio-helpers.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include "obs-module.h"
+#include <util/windows/window-helpers.h>
+
+#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);
+}

+ 2 - 0
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

+ 3 - 0
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."

+ 44 - 2
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,

+ 36 - 3
plugins/win-capture/window-capture.c

@@ -2,7 +2,9 @@
 #include <util/dstr.h>
 #include <util/threading.h>
 #include <util/windows/window-helpers.h>
+
 #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,
 };