diff --git a/docs/sphinx/reference-libobs-media-io.rst b/docs/sphinx/reference-libobs-media-io.rst index 6d7e46f3efb1a9..f5af7c249fdadc 100644 --- a/docs/sphinx/reference-libobs-media-io.rst +++ b/docs/sphinx/reference-libobs-media-io.rst @@ -514,6 +514,23 @@ FFmpeg wrapper to resample audio. --------------------- +.. function:: void audio_resampler_set_compensation_error(audio_resampler_t *resampler, int error_ns) + + Activate resampling compensation and set current error. + + :param resampler: Audio resampler object + :param error_ns: Current error in nanosecond + +--------------------- + +.. function:: void audio_resampler_disable_compensation(audio_resampler_t *resampler) + + Deactivate resampling compensation. + + :param resampler: Audio resampler object + +--------------------- + .. function:: 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) Resamples audio frames. diff --git a/libobs/media-io/audio-resampler-ffmpeg.c b/libobs/media-io/audio-resampler-ffmpeg.c index eb5be4f7435e2e..619f3dda3b2b3a 100644 --- a/libobs/media-io/audio-resampler-ffmpeg.c +++ b/libobs/media-io/audio-resampler-ffmpeg.c @@ -21,6 +21,9 @@ #include #include #include +#include "lag_lead_filter.h" + +// #define DEBUG_COMPENSATION struct audio_resampler { struct SwrContext *context; @@ -41,6 +44,12 @@ struct audio_resampler { AVChannelLayout input_ch_layout; AVChannelLayout output_ch_layout; #endif + + struct lag_lead_filter compensation_filter; + bool compensation_filter_configured; + + uint64_t total_input_samples; + uint64_t total_output_samples; }; static inline enum AVSampleFormat convert_audio_format(enum audio_format format) @@ -169,6 +178,14 @@ audio_resampler_t *audio_resampler_create(const struct resample_info *dst, const void audio_resampler_destroy(audio_resampler_t *rs) { if (rs) { + uint64_t total_output_samples = rs->total_output_samples + swr_get_delay(rs->context, rs->output_freq); + if (rs->compensation_filter_configured) + blog(LOG_INFO, + "audio_resampler (asynchronous compensation): input %" PRIu64 " samples, " + "output %" PRIu64 " samples, estimated input frequency %f Hz", + rs->total_input_samples, rs->total_output_samples, + (double)rs->total_input_samples / total_output_samples * rs->output_freq); + if (rs->context) swr_free(&rs->context); if (rs->output_buffer[0]) @@ -178,6 +195,27 @@ void audio_resampler_destroy(audio_resampler_t *rs) } } +void audio_resampler_set_compensation_error(audio_resampler_t *rs, int error_ns) +{ + if (!rs->compensation_filter_configured) { + // Parameters are determined experimentally by SPICE simulation + // to have these characteristics. + // - Unity gain frequency: 1/180 Hz (inverse of 3 minutes) + // - Phase margin: 60 degrees + lag_lead_filter_set_parameters(&rs->compensation_filter, 3.756e-2, 8.250, 107.1); + lag_lead_filter_reset(&rs->compensation_filter); + rs->compensation_filter_configured = true; + } + + lag_lead_filter_set_error_ns(&rs->compensation_filter, error_ns); +} + +void audio_resampler_disable_compensation(audio_resampler_t *rs) +{ + rs->compensation_filter_configured = false; + swr_set_compensation(rs->context, 0, 0); +} + bool audio_resampler_resample(audio_resampler_t *rs, uint8_t *output[], uint32_t *out_frames, uint64_t *ts_offset, const uint8_t *const input[], uint32_t in_frames) { @@ -187,6 +225,17 @@ bool audio_resampler_resample(audio_resampler_t *rs, uint8_t *output[], uint32_t struct SwrContext *context = rs->context; int ret; + if (rs->compensation_filter_configured) { + lag_lead_filter_tick(&rs->compensation_filter, rs->input_freq, in_frames); + double drift = lag_lead_filter_get_drift(&rs->compensation_filter); + + ret = swr_set_compensation(rs->context, -(int)(drift * 65536 / rs->input_freq), 65536); + if (ret < 0) { + blog(LOG_ERROR, "swr_set_compensation failed: %d", ret); + return false; + } + } + int64_t delay = swr_get_delay(context, rs->input_freq); int estimated = (int)av_rescale_rnd(delay + (int64_t)in_frames, (int64_t)rs->output_freq, (int64_t)rs->input_freq, AV_ROUND_UP); @@ -210,9 +259,20 @@ bool audio_resampler_resample(audio_resampler_t *rs, uint8_t *output[], uint32_t return false; } +#ifdef DEBUG_COMPENSATION + if (rs->compensation_filter_configured) + blog(LOG_INFO, + "async-compensation in_frames=%d out_frames=%d error_ns=%" PRIi64 " internal-condition=(%f %f)", + in_frames, ret, rs->compensation_filter.error_ns, rs->compensation_filter.vc1, + rs->compensation_filter.vc2); +#endif + for (uint32_t i = 0; i < rs->output_planes; i++) output[i] = rs->output_buffer[i]; + rs->total_input_samples += in_frames; + rs->total_output_samples += ret; + *out_frames = (uint32_t)ret; return true; } diff --git a/libobs/media-io/audio-resampler.h b/libobs/media-io/audio-resampler.h index ca1e7b08d58000..6addfaf32e7812 100644 --- a/libobs/media-io/audio-resampler.h +++ b/libobs/media-io/audio-resampler.h @@ -36,6 +36,10 @@ struct resample_info { EXPORT audio_resampler_t *audio_resampler_create(const struct resample_info *dst, const struct resample_info *src); EXPORT void audio_resampler_destroy(audio_resampler_t *resampler); +EXPORT void audio_resampler_set_compensation_error(audio_resampler_t *resampler, int delta_ns_per_s); + +EXPORT void audio_resampler_disable_compensation(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); diff --git a/libobs/media-io/lag_lead_filter.h b/libobs/media-io/lag_lead_filter.h new file mode 100644 index 00000000000000..d673d743b9cbed --- /dev/null +++ b/libobs/media-io/lag_lead_filter.h @@ -0,0 +1,63 @@ +/* + * Lag lead filter for asynchronous sample rate converter + * Copyright (C) 2022-2026 Norihiro Kamae + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +struct lag_lead_filter { + int64_t error_ns; + double vc1; + double vc2; + + double gain; + double c1_inv; + double c2_inv; +}; + +static void lag_lead_filter_reset(struct lag_lead_filter *f) +{ + f->vc1 = 0.0; + f->vc2 = 0.0; +} + +static void lag_lead_filter_set_parameters(struct lag_lead_filter *f, double gain, double c1, double c2) +{ + f->gain = gain; + f->c1_inv = 1.0 / c1; + f->c2_inv = 1.0 / c2; +} + +static inline void lag_lead_filter_set_error_ns(struct lag_lead_filter *f, int32_t error_ns) +{ + f->error_ns = error_ns; +} + +static void lag_lead_filter_tick(struct lag_lead_filter *f, uint32_t fs, uint32_t frames) +{ + const double icp = f->gain * f->error_ns * 1e-9; + const double ic2 = (f->vc1 - f->vc2) / fs; + const double ic1 = icp - ic2; + + f->vc1 += ic1 * f->c1_inv * frames; + f->vc2 += ic2 * f->c2_inv * frames; +} + +static inline double lag_lead_filter_get_drift(const struct lag_lead_filter *f) +{ + return f->vc1; +} diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index d88093eec1cfea..e68b708a7a070c 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -861,6 +861,8 @@ struct obs_source { bool audio_active; bool user_muted; bool muted; + volatile bool async_compensation; + bool last_async_compensation; struct obs_source *next_audio_source; struct obs_source **prev_next_audio_source; uint64_t audio_ts; diff --git a/libobs/obs-source.c b/libobs/obs-source.c index e5ef1304fdb7f4..901a81f1d02648 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -1635,6 +1635,16 @@ static void source_output_audio_data(obs_source_t *source, const struct audio_da } } + if (os_atomic_load_bool(&source->async_compensation) && source->resampler) { + uint64_t ts_buffered = in.timestamp; + uint64_t ts_data = data->timestamp - source->resample_offset; + int64_t error = (int64_t)(ts_buffered - ts_data); + if (push_back && INT32_MIN <= error && error <= INT32_MAX) + audio_resampler_set_compensation_error(source->resampler, (int)error); + else + audio_resampler_set_compensation_error(source->resampler, 0); + } + sync_offset = source->sync_offset; in.timestamp += sync_offset; in.timestamp -= source->resample_offset; @@ -3894,16 +3904,20 @@ static inline void reset_resampler(obs_source_t *source, const struct obs_source output_info.samples_per_sec = obs_info->samples_per_sec; output_info.speakers = obs_info->speakers; + bool async_compensation = os_atomic_load_bool(&source->async_compensation); + source->sample_info.format = audio->format; source->sample_info.samples_per_sec = audio->samples_per_sec; source->sample_info.speakers = audio->speakers; + source->last_async_compensation = async_compensation; audio_resampler_destroy(source->resampler); source->resampler = NULL; source->resample_offset = 0; if (source->sample_info.samples_per_sec == obs_info->samples_per_sec && - source->sample_info.format == obs_info->format && source->sample_info.speakers == obs_info->speakers) { + source->sample_info.format == obs_info->format && source->sample_info.speakers == obs_info->speakers && + !async_compensation) { source->audio_failed = false; return; } @@ -3996,7 +4010,8 @@ static void process_audio(obs_source_t *source, const struct obs_source_audio *a bool mono_output; if (source->sample_info.samples_per_sec != audio->samples_per_sec || - source->sample_info.format != audio->format || source->sample_info.speakers != audio->speakers) + source->sample_info.format != audio->format || source->sample_info.speakers != audio->speakers || + source->last_async_compensation != os_atomic_load_bool(&source->async_compensation)) reset_resampler(source, audio); if (source->audio_failed) @@ -5608,6 +5623,21 @@ bool obs_source_async_unbuffered(const obs_source_t *source) return obs_source_valid(source, "obs_source_async_unbuffered") ? source->async_unbuffered : false; } +void obs_source_set_async_compensation(obs_source_t *source, bool compensate) +{ + if (!obs_source_valid(source, "obs_source_set_async_compensation")) + return; + + os_atomic_store_bool(&source->async_compensation, compensate); +} + +bool obs_source_async_compensation(const obs_source_t *source) +{ + return obs_source_valid(source, "obs_source_async_compensation") + ? os_atomic_load_bool(&source->async_compensation) + : false; +} + obs_data_t *obs_source_get_private_settings(obs_source_t *source) { if (!obs_ptr_valid(source, "obs_source_get_private_settings")) diff --git a/libobs/obs.h b/libobs/obs.h index 57844b05694cec..8f875e76ce0b47 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -1544,6 +1544,9 @@ EXPORT void obs_source_get_audio_mix(const obs_source_t *source, struct obs_sour EXPORT void obs_source_set_async_unbuffered(obs_source_t *source, bool unbuffered); EXPORT bool obs_source_async_unbuffered(const obs_source_t *source); +EXPORT void obs_source_set_async_compensation(obs_source_t *source, bool compensate); +EXPORT bool obs_source_async_compensation(const obs_source_t *source); + /** Used to decouple audio from video so that audio doesn't attempt to sync up * with video. I.E. Audio acts independently. Only works when in unbuffered * mode. */ diff --git a/plugins/aja/aja-source.cpp b/plugins/aja/aja-source.cpp index 1aabc3b3620879..38af253256269e 100644 --- a/plugins/aja/aja-source.cpp +++ b/plugins/aja/aja-source.cpp @@ -860,6 +860,8 @@ static void aja_source_update(void *data, obs_data_t *settings) ajaSource->Deactivate(); } + obs_source_set_async_compensation(ajaSource->GetOBSSource(), obs_data_get_bool(settings, "async_compensation")); + auto &cardManager = aja::CardManager::Instance(); cardManager.EnumerateCards(); auto cardEntry = cardManager.GetCardEntry(wantCardID); @@ -1015,6 +1017,8 @@ static obs_properties_t *aja_source_get_properties(void *data) obs_property_set_modified_callback2(device_list, aja_source_device_changed, data); obs_property_set_modified_callback2(io_select_list, aja_io_selection_changed, data); + obs_properties_add_bool(props, "async_compensation", obs_module_text("AsyncCompensation")); + return props; } @@ -1029,6 +1033,7 @@ void aja_source_get_defaults(obs_data_t *settings) obs_data_set_default_int(settings, kUIPropChannelFormat.id, kDefaultAudioCaptureChannels); obs_data_set_default_bool(settings, kUIPropChannelSwap_FC_LFE.id, false); obs_data_set_default_bool(settings, kUIPropDeactivateWhenNotShowing.id, false); + obs_data_set_default_bool(settings, "async_compensation", true); } static void aja_source_get_defaults_v1(obs_data_t *settings) diff --git a/plugins/aja/data/locale/en-US.ini b/plugins/aja/data/locale/en-US.ini index 88c4430c46e12c..a26ce5ba566692 100644 --- a/plugins/aja/data/locale/en-US.ini +++ b/plugins/aja/data/locale/en-US.ini @@ -25,3 +25,4 @@ ChannelFormat.5_1ch="5.1ch" ChannelFormat.7_1ch="7.1ch" SwapFC-LFE="Swap FC and LFE" SwapFC-LFE.Tooltip="Swap Front Center Channel and LFE Channel" +AsyncCompensation="Enable Asynchronous Compensation" diff --git a/plugins/decklink/const.h b/plugins/decklink/const.h index 9a2d094865f6d3..cfb53be69bfacd 100644 --- a/plugins/decklink/const.h +++ b/plugins/decklink/const.h @@ -15,6 +15,7 @@ #define KEYER "keyer" #define SWAP "swap" #define ALLOW_10_BIT "allow_10_bit" +#define ASYNC_COMPENSATION "async_compensation" #define TEXT_DEVICE obs_module_text("Device") #define TEXT_VIDEO_CONNECTION obs_module_text("VideoConnection") @@ -43,3 +44,4 @@ #define TEXT_SWAP obs_module_text("SwapFC-LFE") #define TEXT_SWAP_TOOLTIP obs_module_text("SwapFC-LFE.Tooltip") #define TEXT_ALLOW_10_BIT obs_module_text("Allow10Bit") +#define TEXT_ASYNC_COMPENSATION obs_module_text("AsyncCompensation") diff --git a/plugins/decklink/data/locale/en-US.ini b/plugins/decklink/data/locale/en-US.ini index d12fc8cb2156bd..22d39daef30e3a 100644 --- a/plugins/decklink/data/locale/en-US.ini +++ b/plugins/decklink/data/locale/en-US.ini @@ -25,3 +25,4 @@ SwapFC-LFE.Tooltip="Swap Front Center Channel and LFE Channel" VideoConnection="Video Connection" AudioConnection="Audio Connection" Allow10Bit="Allow 10 Bit (Required for SDI captions, may cause performance overhead)" +AsyncCompensation="Enable Asynchronous Compensation" diff --git a/plugins/decklink/decklink-source.cpp b/plugins/decklink/decklink-source.cpp index 6dcf6a7b5f1e66..ff7b01f7eadc68 100644 --- a/plugins/decklink/decklink-source.cpp +++ b/plugins/decklink/decklink-source.cpp @@ -71,6 +71,8 @@ static void decklink_update(void *data, obs_data_t *settings) decklink->swap = obs_data_get_bool(settings, SWAP); decklink->allow10Bit = obs_data_get_bool(settings, ALLOW_10_BIT); decklink->Activate(device, id, videoConnection, audioConnection); + + obs_source_set_async_compensation(decklink->GetSource(), obs_data_get_bool(settings, ASYNC_COMPENSATION)); } static void decklink_show(void *data) @@ -99,6 +101,7 @@ static void decklink_get_defaults(obs_data_t *settings) obs_data_set_default_int(settings, COLOR_RANGE, VIDEO_RANGE_DEFAULT); obs_data_set_default_int(settings, CHANNEL_FORMAT, SPEAKERS_STEREO); obs_data_set_default_bool(settings, SWAP, false); + obs_data_set_default_bool(settings, ASYNC_COMPENSATION, true); } static const char *decklink_get_name(void *) @@ -261,6 +264,8 @@ static obs_properties_t *decklink_get_properties(void *data) obs_properties_add_bool(props, ALLOW_10_BIT, TEXT_ALLOW_10_BIT); + obs_properties_add_bool(props, ASYNC_COMPENSATION, TEXT_ASYNC_COMPENSATION); + UNUSED_PARAMETER(data); return props; } diff --git a/plugins/linux-alsa/alsa-input.c b/plugins/linux-alsa/alsa-input.c index cb6d79dce9920d..0ca865f9dc8854 100644 --- a/plugins/linux-alsa/alsa-input.c +++ b/plugins/linux-alsa/alsa-input.c @@ -110,6 +110,7 @@ static enum speaker_layout _alsa_channels_to_obs_speakers(unsigned int); void *alsa_create(obs_data_t *settings, obs_source_t *source) { struct alsa_data *data = bzalloc(sizeof(struct alsa_data)); + bool async_compensation; data->source = source; #if SHUTDOWN_ON_DEACTIVATE @@ -140,6 +141,10 @@ void *alsa_create(obs_data_t *settings, obs_source_t *source) #if !SHUTDOWN_ON_DEACTIVATE _alsa_try_open(data); #endif + + async_compensation = obs_data_get_bool(settings, "async_compensation"); + obs_source_set_async_compensation(data->source, async_compensation); + return data; cleanup: @@ -186,6 +191,7 @@ void alsa_update(void *vptr, obs_data_t *settings) struct alsa_data *data = vptr; const char *device; unsigned int rate; + bool async_compensation; bool reset = false; device = obs_data_get_string(settings, "device_id"); @@ -218,6 +224,9 @@ void alsa_update(void *vptr, obs_data_t *settings) _alsa_try_open(data); } #endif + + async_compensation = obs_data_get_bool(settings, "async_compensation"); + obs_source_set_async_compensation(data->source, async_compensation); } const char *alsa_get_name(void *unused) @@ -231,6 +240,7 @@ void alsa_get_defaults(obs_data_t *settings) obs_data_set_default_string(settings, "device_id", "default"); obs_data_set_default_string(settings, "custom_pcm", "default"); obs_data_set_default_int(settings, "rate", 44100); + obs_data_set_default_bool(settings, "async_compensation", true); } static bool alsa_devices_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings) @@ -333,6 +343,8 @@ obs_properties_t *alsa_get_properties(void *unused) snd_device_name_free_hint(hints); + obs_properties_add_bool(props, "async_compensation", obs_module_text("AsyncCompensation")); + return props; } diff --git a/plugins/linux-alsa/data/locale/en-US.ini b/plugins/linux-alsa/data/locale/en-US.ini index 244797d9fc0a47..fc3d496004f67a 100644 --- a/plugins/linux-alsa/data/locale/en-US.ini +++ b/plugins/linux-alsa/data/locale/en-US.ini @@ -4,3 +4,4 @@ Custom="Custom" Device="Device" PCM="PCM" Rate="Rate" +AsyncCompensation="Enable Asynchronous Compensation" diff --git a/plugins/linux-pulseaudio/data/locale/en-US.ini b/plugins/linux-pulseaudio/data/locale/en-US.ini index 43718c05748b3b..4689dfbae4abc2 100644 --- a/plugins/linux-pulseaudio/data/locale/en-US.ini +++ b/plugins/linux-pulseaudio/data/locale/en-US.ini @@ -2,3 +2,4 @@ PulseInput="Audio Input Capture (PulseAudio)" PulseOutput="Audio Output Capture (PulseAudio)" Device="Device" Default="Default" +AsyncCompensation="Enable Asynchronous Compensation" diff --git a/plugins/linux-pulseaudio/pulse-input.c b/plugins/linux-pulseaudio/pulse-input.c index 591446fe0f4dc7..d389fad904e40f 100644 --- a/plugins/linux-pulseaudio/pulse-input.c +++ b/plugins/linux-pulseaudio/pulse-input.c @@ -455,6 +455,8 @@ static obs_properties_t *pulse_properties(bool input) if (count > 0) obs_property_list_insert_string(devices, 0, obs_module_text("Default"), "default"); + obs_properties_add_bool(props, "async_compensation", obs_module_text("AsyncCompensation")); + return props; } @@ -478,6 +480,7 @@ static obs_properties_t *pulse_output_properties(void *unused) static void pulse_defaults(obs_data_t *settings) { obs_data_set_default_string(settings, "device_id", "default"); + obs_data_set_default_bool(settings, "async_compensation", true); } /** @@ -540,6 +543,9 @@ static void pulse_update(void *vptr, obs_data_t *settings) restart = true; } + bool async_compensation = obs_data_get_bool(settings, "async_compensation"); + obs_source_set_async_compensation(data->source, async_compensation); + if (!restart) return; diff --git a/plugins/mac-capture/data/locale/en-US.ini b/plugins/mac-capture/data/locale/en-US.ini index 40c62a4e3604fc..9efafa8798efd5 100644 --- a/plugins/mac-capture/data/locale/en-US.ini +++ b/plugins/mac-capture/data/locale/en-US.ini @@ -37,3 +37,4 @@ SCK.AudioUnavailable="Audio capture requires macOS 13 or newer." SCK.CaptureTypeUnavailable="Selected capture type requires macOS 13 or newer." SCK.Method="Method" SCK.Restart="Restart capture" +AsyncCompensation="Enable Asynchronous Compensation" diff --git a/plugins/mac-capture/mac-audio.c b/plugins/mac-capture/mac-audio.c index 8d9dca9972fbe7..1fa9c3f00efdf9 100644 --- a/plugins/mac-capture/mac-audio.c +++ b/plugins/mac-capture/mac-audio.c @@ -837,6 +837,9 @@ static void coreaudio_update(void *data, obs_data_t *settings) coreaudio_set_channels(ca, settings); } + bool async_compensation = obs_data_get_bool(settings, "async_compensation"); + obs_source_set_async_compensation(ca->source, async_compensation); + coreaudio_try_init(ca); } @@ -844,6 +847,7 @@ 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); + obs_data_set_default_bool(settings, "async_compensation", true); } static void *coreaudio_create(obs_data_t *settings, obs_source_t *source, bool input) @@ -1027,6 +1031,8 @@ static obs_properties_t *coreaudio_properties(bool input, void *data) } device_list_free(&devices); + + obs_properties_add_bool(props, "async_compensation", obs_module_text("AsyncCompensation")); return props; } diff --git a/plugins/win-wasapi/data/locale/en-US.ini b/plugins/win-wasapi/data/locale/en-US.ini index 58531fc864eb88..bf0cda594a65d0 100644 --- a/plugins/win-wasapi/data/locale/en-US.ini +++ b/plugins/win-wasapi/data/locale/en-US.ini @@ -9,3 +9,4 @@ 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" +AsyncCompensation="Enable Asynchronous Compensation" diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index 0cea4758559b2f..679caca2b35ec0 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -28,6 +28,7 @@ using namespace std; #define OPT_USE_DEVICE_TIMING "use_device_timing" #define OPT_WINDOW "window" #define OPT_PRIORITY "priority" +#define OPT_ASYNC_COMPENSATION "async_compensation" WASAPINotify *GetNotify(); static void GetWASAPIDefaults(obs_data_t *settings); @@ -255,6 +256,7 @@ class WASAPISource { string device_id; bool useDeviceTiming; bool isDefaultDevice; + bool async_compensation; window_priority priority; string window_class; string title; @@ -477,6 +479,7 @@ WASAPISource::UpdateParams WASAPISource::BuildUpdateParams(obs_data_t *settings) params.device_id = obs_data_get_string(settings, OPT_DEVICE_ID); params.useDeviceTiming = obs_data_get_bool(settings, OPT_USE_DEVICE_TIMING); params.isDefaultDevice = _strcmpi(params.device_id.c_str(), "default") == 0; + params.async_compensation = obs_data_get_bool(settings, OPT_ASYNC_COMPENSATION); params.priority = (window_priority)obs_data_get_int(settings, "priority"); params.window_class.clear(); params.title.clear(); @@ -517,6 +520,8 @@ void WASAPISource::UpdateSettings(UpdateParams &¶ms) window_class = std::move(params.window_class); title = std::move(params.title); executable = std::move(params.executable); + + obs_source_set_async_compensation(source, params.async_compensation & !params.useDeviceTiming); } void WASAPISource::LogSettings() @@ -1331,6 +1336,7 @@ 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, OPT_ASYNC_COMPENSATION, true); } static void GetWASAPIDefaultsDeviceOutput(obs_data_t *settings) @@ -1461,6 +1467,16 @@ static bool UpdateWASAPIMethod(obs_properties_t *props, obs_property_t *, obs_da return true; } +static bool device_timing_changed(obs_properties_t *props, obs_property_t *property, obs_data_t *settings) +{ + bool useDeviceTiming = obs_data_get_bool(settings, OPT_USE_DEVICE_TIMING); + + obs_property_t *prop = obs_properties_get(props, OPT_ASYNC_COMPENSATION); + obs_property_set_enabled(prop, !useDeviceTiming); + + return true; +} + static obs_properties_t *GetWASAPIPropertiesInput(void *) { obs_properties_t *props = obs_properties_create(); @@ -1479,7 +1495,12 @@ static obs_properties_t *GetWASAPIPropertiesInput(void *) obs_property_list_add_string(device_prop, device.name.c_str(), device.id.c_str()); } - obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); + obs_property_t *device_timing_prop = + obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); + + obs_property_set_modified_callback(device_timing_prop, device_timing_changed); + + obs_properties_add_bool(props, OPT_ASYNC_COMPENSATION, obs_module_text("AsyncCompensation")); return props; } @@ -1502,7 +1523,12 @@ static obs_properties_t *GetWASAPIPropertiesDeviceOutput(void *) obs_property_list_add_string(device_prop, device.name.c_str(), device.id.c_str()); } - obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); + obs_property_t *device_timing_prop = + obs_properties_add_bool(props, OPT_USE_DEVICE_TIMING, obs_module_text("UseDeviceTiming")); + + obs_property_set_modified_callback(device_timing_prop, device_timing_changed); + + obs_properties_add_bool(props, OPT_ASYNC_COMPENSATION, obs_module_text("AsyncCompensation")); return props; } @@ -1535,6 +1561,8 @@ static obs_properties_t *GetWASAPIPropertiesProcessOutput(void *data) obs_property_list_add_int(priority_prop, obs_module_text("Priority.Class"), WINDOW_PRIORITY_CLASS); obs_property_list_add_int(priority_prop, obs_module_text("Priority.Exe"), WINDOW_PRIORITY_EXE); + obs_properties_add_bool(props, OPT_ASYNC_COMPENSATION, obs_module_text("AsyncCompensation")); + return props; }