Browse Source

libobs: Improve monitoring deduplication

The monitoring deduplication was previously checking at each audio tick
the whole audio tree for Audio Output Capture (AOC) devices used for
monitoring. The profiling did not show any big impact on the audio
callback. But due to reports of a significant slowdown for scenes with
numerous audio sources, we have moved the check on AOC from the audio
thread to the UI thread.

So we implemented obs_source_audio_output_capture_device_changed which
is triggered whenever:
- a monitoring device is changed in Settings > Audio,
- an Audio Output Capture source changes its device or on startup.

This function compares the AOC device with the monitoring device in UI
thread. If they are identical, a signal is sent to the audio thread to
add an audio task updating the AOC duplicating source pointer at the end
of an audio tick.

This triggers monitoring deduplication if the ptr is not NULL.
The calls in the AOC are implemented in next commits.
The rest of the logic in obs-audio.c is the same except on one count,
which is that we check against the muted state of the AOC rather than
its user_muted; with the new logic, muted works better.

Signed-off-by: pkv <[email protected]>
pkv 1 week ago
parent
commit
91917f8120
5 changed files with 114 additions and 70 deletions
  1. 6 66
      libobs/obs-audio.c
  2. 0 2
      libobs/obs-internal.h
  3. 65 0
      libobs/obs-source.c
  4. 36 2
      libobs/obs.c
  5. 7 0
      libobs/obs.h

+ 6 - 66
libobs/obs-audio.c

@@ -48,54 +48,6 @@ static inline bool is_individual_audio_source(obs_source_t *source)
 	       !(source->info.output_flags & OBS_SOURCE_COMPOSITE);
 }
 
-extern bool devices_match(const char *id1, const char *id2);
-
-static inline void check_audio_output_source_is_monitoring_device(obs_source_t *s, void *p)
-{
-	struct obs_core_audio *audio = p;
-	if (!audio->monitoring_device_name)
-		return;
-
-	const char *id = s->info.id;
-	if (strcmp(id, "wasapi_output_capture") != 0 && strcmp(id, "pulse_output_capture") != 0 &&
-	    strcmp(id, "coreaudio_output_capture") != 0)
-		return;
-
-	obs_data_t *settings = obs_source_get_settings(s);
-	if (!settings)
-		return;
-
-	const char *device_id = obs_data_get_string(settings, "device_id");
-	const char *mon_id = audio->monitoring_device_id;
-	bool id_match = false;
-
-#ifdef __APPLE__
-	extern void get_desktop_default_id(char **p_id);
-	if (device_id && strcmp(device_id, "default") == 0) {
-		char *def_id = NULL;
-		get_desktop_default_id(&def_id);
-		id_match = devices_match(def_id, mon_id);
-		if (def_id)
-			bfree(def_id);
-	} else {
-		id_match = devices_match(device_id, mon_id);
-	}
-#else
-	id_match = devices_match(device_id, mon_id);
-#endif
-
-	if (id_match) {
-		audio->prevent_monitoring_duplication = true;
-		audio->monitoring_duplicating_source = s;
-		if (!audio->monitoring_duplication_prevented_on_prev_tick)
-			blog(LOG_INFO, "Device for 'Audio Output Capture' source is also used for audio"
-				       " monitoring:\nDeduplication logic is being applied to all monitored"
-				       " sources.\n");
-	}
-
-	obs_data_release(settings);
-}
-
 /*
  * This version of push_audio_tree checks whether any source is an Audio Output Capture source ('Desktop Audio',
  * 'wasapi_output_capture' on Windows, 'pulse_output_capture' on Linux, 'coreaudio_output_capture' on macOS), & if the
@@ -118,7 +70,6 @@ static void push_audio_tree2(obs_source_t *parent, obs_source_t *source, void *p
 		if (s) {
 			da_push_back(audio->render_order, &s);
 			s->audio_is_duplicated = false;
-			check_audio_output_source_is_monitoring_device(s, audio);
 		}
 	} else {
 		/* Source already present in tree → mark as duplicated if applicable */
@@ -558,23 +509,18 @@ static inline void execute_audio_tasks(void)
 
 /* In case monitoring and an 'Audio Output Capture' source have the same device, one silences all the monitored
  * sources unless the 'Audio Output Capture' is muted.
- * The syncing between the mute state of the 'Audio Output Capture' source set in UI and libobs is a bit tricky. There 
- * is an intrinsic positive delay of the 'Audio Output Capture' source with respect to monitored sources due to the 
- * audio OS processing (wasapi, pulseaudio or coreaudio). Unfortunately, the delay is machine dependent and we can only 
- * mitigate its effects. During tests, src->user_muted worked better than src->muted, maybe because it is set earlier 
- * than the muted flag which allows to keep a better sync. With src->muted, unmuting the 'Audio Output Capture' led to
- * a systematic level increase during one tick during testing for a sine tone (about +3 dBFS). In general we
- * don't expect much difference though between user_muted and muted.
  */
 static inline bool should_silence_monitored_source(obs_source_t *source, struct obs_core_audio *audio)
 {
-	if (!audio->monitoring_duplicating_source)
+	obs_source_t *dup_src = audio->monitoring_duplicating_source;
+
+	if (!dup_src || !obs_source_active(dup_src))
 		return false;
 
 	bool fader_muted = close_float(audio->monitoring_duplicating_source->volume, 0.0f, 0.0001f);
-	bool output_capture_unmuted = !audio->monitoring_duplicating_source->user_muted && !fader_muted;
+	bool output_capture_unmuted = !audio->monitoring_duplicating_source->muted && !fader_muted;
 
-	if (audio->prevent_monitoring_duplication && output_capture_unmuted) {
+	if (output_capture_unmuted) {
 		if (source->monitoring_type == OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT &&
 		    source != audio->monitoring_duplicating_source) {
 			return true;
@@ -617,9 +563,6 @@ bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in, uint6
 
 	da_resize(audio->render_order, 0);
 	da_resize(audio->root_nodes, 0);
-	audio->monitoring_duplication_prevented_on_prev_tick = audio->prevent_monitoring_duplication;
-	audio->prevent_monitoring_duplication = false;
-	audio->monitoring_duplicating_source = NULL;
 
 	deque_push_back(&audio->buffered_timestamps, &ts, sizeof(ts));
 	deque_peek_front(&audio->buffered_timestamps, &ts, sizeof(ts));
@@ -656,14 +599,11 @@ bool audio_callback(void *param, uint64_t start_ts_in, uint64_t end_ts_in, uint6
 			if (obs->video.mixes.array[j]->mix_audio)
 				da_push_back(audio->root_nodes, &source);
 
-			/* Build audio tree, track Audio Output Capture sources and tag duplicate individual sources */
+			/* Build audio tree, tag duplicate individual sources */
 			obs_source_enum_active_tree(source, push_audio_tree2, audio);
 
 			/* add top - level sources to audio tree */
 			push_audio_tree(NULL, source, audio);
-
-			/* Check whether the source is an 'Audio Output Capture' and coincides with monitoring device */
-			check_audio_output_source_is_monitoring_device(source, audio);
 		}
 		pthread_mutex_unlock(&view->channels_mutex);
 	}

+ 0 - 2
libobs/obs-internal.h

@@ -460,9 +460,7 @@ struct obs_core_audio {
 	pthread_mutex_t task_mutex;
 	struct deque tasks;
 
-	volatile bool prevent_monitoring_duplication;
 	struct obs_source *monitoring_duplicating_source;
-	bool monitoring_duplication_prevented_on_prev_tick;
 };
 
 /* user sources, output channels, and displays */

+ 65 - 0
libobs/obs-source.c

@@ -368,6 +368,71 @@ static void obs_source_init_audio_hotkeys(struct obs_source *source)
 							      obs_source_hotkey_push_to_talk, source);
 }
 
+void obs_source_audio_output_capture_device_activated(void *vptr, calldata_t *cd)
+{
+	UNUSED_PARAMETER(vptr);
+	obs_source_t *src = calldata_ptr(cd, "source");
+	if (!src)
+		return;
+
+	obs_data_t *settings = obs_source_get_settings(src);
+	const char *device_id = obs_data_get_string(settings, "device_id");
+	obs_source_audio_output_capture_device_changed(src, device_id);
+	obs_data_release(settings);
+}
+
+extern bool devices_match(const char *id1, const char *id2);
+void obs_source_audio_output_capture_device_changed(obs_source_t *src, const char *device_id)
+{
+	struct obs_core_audio *audio = &obs->audio;
+
+	if (!audio->monitoring_device_name)
+		return;
+
+	if (!(src->info.output_flags & OBS_SOURCE_DO_NOT_SELF_MONITOR))
+		return;
+
+	const char *mon_id = audio->monitoring_device_id;
+	bool id_match = false;
+
+#ifdef __APPLE__
+	extern void get_desktop_default_id(char **p_id);
+	if (device_id && strcmp(device_id, "default") == 0) {
+		char *def_id = NULL;
+		get_desktop_default_id(&def_id);
+		id_match = devices_match(def_id, mon_id);
+		if (def_id)
+			bfree(def_id);
+	} else {
+		id_match = devices_match(device_id, mon_id);
+	}
+#else
+	id_match = devices_match(device_id, mon_id);
+#endif
+	struct calldata cd;
+	uint8_t stack[128];
+	calldata_init_fixed(&cd, stack, sizeof(stack));
+
+	if (id_match) {
+		calldata_set_ptr(&cd, "source", src);
+		signal_handler_signal(obs->signals, "deduplication_changed", &cd);
+		signal_handler_connect(src->context.signals, "activate",
+				       obs_source_audio_output_capture_device_activated, NULL);
+		blog(LOG_INFO,
+		     "Device for 'Audio Output Capture' source %s is also used for audio monitoring."
+		     "\nDeduplication logic is being applied to all monitored sources.",
+		     src->context.name);
+	} else {
+		if (src == audio->monitoring_duplicating_source) {
+			calldata_set_ptr(&cd, "source", NULL);
+			signal_handler_disconnect(src->context.signals, "activate",
+						  obs_source_audio_output_capture_device_activated, NULL);
+			signal_handler_signal(obs->signals, "deduplication_changed", &cd);
+			blog(LOG_INFO, "Deduplication logic stopped.");
+		}
+	}
+}
+
 static obs_source_t *obs_source_create_internal(const char *id, const char *name, const char *uuid,
 						obs_data_t *settings, obs_data_t *hotkey_data, bool private,
 						uint32_t last_obs_ver, obs_canvas_t *canvas)

+ 36 - 2
libobs/obs.c

@@ -880,6 +880,22 @@ static void obs_free_graphics(void)
 	}
 }
 
+void set_monitoring_duplication_source(void *param)
+{
+	obs_source_t *src = param;
+	struct obs_core_audio *audio = &obs->audio;
+
+	audio->monitoring_duplicating_source = src;
+}
+
+static void apply_monitoring_deduplication(void *ignored, calldata_t *cd)
+{
+	UNUSED_PARAMETER(ignored);
+	obs_source_t *src = calldata_ptr(cd, "source");
+
+	obs_queue_task(OBS_TASK_AUDIO, set_monitoring_duplication_source, src, false);
+}
+
 static void set_audio_thread(void *unused);
 
 static bool obs_init_audio(struct audio_output_info *ai)
@@ -899,7 +915,10 @@ static bool obs_init_audio(struct audio_output_info *ai)
 
 	audio->monitoring_device_name = bstrdup("Default");
 	audio->monitoring_device_id = bstrdup("default");
-	audio->monitoring_duplication_prevented_on_prev_tick = false;
+	audio->monitoring_duplicating_source = NULL;
+
+	signal_handler_add(obs->signals, "void deduplication_changed(ptr source)");
+	signal_handler_connect(obs->signals, "deduplication_changed", apply_monitoring_deduplication, NULL);
 
 	errorcode = audio_output_open(&audio->audio, ai);
 	if (errorcode == AUDIO_OUTPUT_SUCCESS)
@@ -2947,6 +2966,18 @@ void obs_reset_audio_monitoring(void)
 	pthread_mutex_unlock(&obs->audio.monitoring_mutex);
 }
 
+static bool check_all_aoc_sources(void *param, obs_source_t *src)
+{
+	UNUSED_PARAMETER(param);
+	if (src->info.output_flags & OBS_SOURCE_DO_NOT_SELF_MONITOR) {
+		obs_data_t *settings = obs_source_get_settings(src);
+		const char *device_id = obs_data_get_string(settings, "device_id");
+		obs_source_audio_output_capture_device_changed(src, device_id);
+		obs_data_release(settings);
+	}
+	return true;
+}
+
 bool obs_set_audio_monitoring_device(const char *name, const char *id)
 {
 	if (!name || !id || !*name || !*id)
@@ -2967,10 +2998,13 @@ bool obs_set_audio_monitoring_device(const char *name, const char *id)
 
 	obs->audio.monitoring_device_name = bstrdup(name);
 	obs->audio.monitoring_device_id = bstrdup(id);
+	pthread_mutex_unlock(&obs->audio.monitoring_mutex);
 
 	obs_reset_audio_monitoring();
 
-	pthread_mutex_unlock(&obs->audio.monitoring_mutex);
+	/* Check all Audio Output Capture sources for monitoring duplication. */
+	obs_enum_sources(check_all_aoc_sources, NULL);
+
 	return true;
 }
 

+ 7 - 0
libobs/obs.h

@@ -1323,6 +1323,13 @@ EXPORT void obs_source_add_audio_capture_callback(obs_source_t *source, obs_sour
 EXPORT void obs_source_remove_audio_capture_callback(obs_source_t *source, obs_source_audio_capture_t callback,
 						     void *param);
 
+/**
+ * For an Audio Output Capture source (like 'wasapi_output_capture') used for 'Desktop Audio', this checks whether the
+ * device is also used for monitoring. A signal to obs core struct is then emitted to trigger deduplication  logic at
+ * the end of an audio tick.
+ */
+EXPORT void obs_source_audio_output_capture_device_changed(obs_source_t *source, const char *device_id);
+
 typedef void (*obs_source_caption_t)(void *param, obs_source_t *source, const struct obs_source_cea_708 *captions);
 
 EXPORT void obs_source_add_caption_callback(obs_source_t *source, obs_source_caption_t callback, void *param);