Compare commits

..

9 Commits

Author SHA1 Message Date
WarmUpTill
c8f4e524ba Make deps/exprtk dependency optional 2026-09-28 21:10:59 +02:00
WarmUpTill
b104f98d02 Fix Twitch token status not reflecting reality when cached user ID empty 2026-09-25 19:43:24 +02:00
WarmUpTill
abc8c3708f Fix Twitch token being treated as invalid after load with broken config
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-09-23 09:32:30 +02:00
TheRealAlexV
3b63db2bde Fix crash in SendWebsocketVendorEvent during OBS shutdown
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Guard the vendor event emission with obs_get_module("obs-websocket").
During obs_shutdown() modules are unloaded one-by-one; obs-websocket is
unloaded before advanced-scene-switcher, so the proc handler pointer (_ph)
cached in obs-websocket-api.h is already stale when FreeSceneSwitcher()
runs its stop steps. proc_handler_call() then dereferences freed memory
(use-after-free in pthread_mutex_unlock).

The existing OBSIsShuttingDown() guard does not catch this path: the
forced-exit that follows a "Source Cleanup Error" (scene collection
switch) never fires the SCRIPTING_SHUTDOWN frontend event, so the
plugin's obsIsShuttingDown flag is not set. obs_get_module() returns NULL
once the module is unloaded, which is the reliable signal that
obs-websocket's proc handler is gone.

Refs: #1404
2026-09-13 11:08:04 +02:00
WarmUpTill
fdc806be40 Convert dock window name field into a combo box
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Should make it easier to combine docks into a single dock window without
having to know the exact dock window titles beforehand
2026-09-08 22:52:43 +02:00
WarmUpTill
01107ee858 Fix crash on shutdown 2026-09-08 22:48:13 +02:00
WarmUpTill
be8bc4a63f Add mono option 2026-09-08 22:48:13 +02:00
WarmUpTill
8a603b1675 Add audio condition check if source is enabled on a specific track
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-09-04 22:17:49 +02:00
WarmUpTill
67dda18929 Fix crash when changing scene collection on MacOS 2026-09-04 21:57:47 +02:00
17 changed files with 343 additions and 197 deletions

View File

@@ -383,8 +383,18 @@ target_precompile_headers(${LIB_NAME} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/pch.hpp")
target_include_directories(
${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib"
"${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib")
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk/exprtk.hpp")
target_include_directories(${LIB_NAME}
PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
target_compile_definitions(${LIB_NAME} PRIVATE EXPRTK_SUPPORT=1)
else()
message(
STATUS
"${PROJECT_NAME}: deps/exprtk not found - math expression support disabled"
)
endif()
if(NOT nlohmann_json_DIR
AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/deps/json/CMakeLists.txt")

View File

@@ -267,6 +267,7 @@ AdvSceneSwitcher.macroTab.pauseStateSaveBehavior.unpause="Unpaused"
AdvSceneSwitcher.macroTab.currentRegisterDock="Register dock widget to control the pause state of selected macro or run it manually"
AdvSceneSwitcher.macroTab.currentIsStandaloneDock="Is standalone dock"
AdvSceneSwitcher.macroTab.currentDockWindowName="Add to macro dock with name:"
AdvSceneSwitcher.macroTab.currentDockWindowName.addNew="Add new dock..."
AdvSceneSwitcher.macroTab.currentDockAddRunButton="Add button to run the macro"
AdvSceneSwitcher.macroTab.currentDockAddPauseButton="Add button to pause or unpause the macro"
AdvSceneSwitcher.macroTab.currentDockAddStatusLabel="Add status label"
@@ -342,7 +343,9 @@ AdvSceneSwitcher.condition.audio.type.volume="Configured volume level"
AdvSceneSwitcher.condition.audio.type.syncOffset="Sync offset"
AdvSceneSwitcher.condition.audio.type.monitor="Audio monitoring"
AdvSceneSwitcher.condition.audio.type.balance="Audio balance"
AdvSceneSwitcher.condition.audio.type.track="Audio track"
AdvSceneSwitcher.condition.audio.entry="{{checkType}}of{{audioSources}}is{{condition}}{{volume}}{{volumeDB}}{{percentDBToggle}}{{syncOffset}}{{monitorTypes}}"
AdvSceneSwitcher.condition.audio.layout.track="{{audioSources}}is enabled on{{checkType}}{{track}}"
AdvSceneSwitcher.condition.cursor="Cursor"
AdvSceneSwitcher.condition.cursor.type.region="is in region"
AdvSceneSwitcher.condition.cursor.type.moving="is moving"
@@ -983,6 +986,7 @@ AdvSceneSwitcher.action.playAudio.monitorUnavailable="Audio monitoring not avail
AdvSceneSwitcher.action.playAudio.tracks="Tracks"
AdvSceneSwitcher.action.playAudio.layout.startOffset="{{useStartOffset}}Start at{{startOffset}}"
AdvSceneSwitcher.action.playAudio.layout.playbackDuration="{{useDuration}}Play for{{playbackDuration}}(0 = until end)"
AdvSceneSwitcher.action.playAudio.mono="Mono"
AdvSceneSwitcher.action.recording="Recording"
AdvSceneSwitcher.action.recording.type.stop="Stop recording"
AdvSceneSwitcher.action.recording.type.start="Start recording"
@@ -2374,6 +2378,7 @@ AdvSceneSwitcher.tempVar.audio.muted="Source muted"
AdvSceneSwitcher.tempVar.audio.sync_offset="Source audio sync offset"
AdvSceneSwitcher.tempVar.audio.monitor="Source audio monitor type"
AdvSceneSwitcher.tempVar.audio.balance="Source audio balance"
AdvSceneSwitcher.tempVar.audio.trackEnabled="Audio track enabled"
AdvSceneSwitcher.tempVar.clipboard.mimeType.primary="Primary clipboard item MIME type"
AdvSceneSwitcher.tempVar.clipboard.mimeType.primary.description="Highest priority MIME type of the current item stored in clipboard, if available."
@@ -3123,6 +3128,3 @@ AdvSceneSwitcher.sceneGroupTab.defaultname="Scene Group %1"
AdvSceneSwitcher.sceneGroupTab.exists="Scene Group or Scene name exists already"
AdvSceneSwitcher.sceneGroupTab.help="Scene Groups can be selected as a target just like a regular scene.\n\nAs the name suggests a scene group is a collection of multiple scenes.\nThe scene group will advance through the list of its assigned scenes depending on the configured settings, which can be found on the right side.\n\nYou can configure the scene group to advance to the next scene in the list:\nAfter a number of times the scene group is selected as a target.\nAfter a certain amount of time has passed.\nOr randomly.\n\nFor example, a scene group containing the scenes ...\nScene 1\nScene 2\nScene 3 \n... will activate \"Scene 1\" the first time it is selected as a target.\nThe second time it will activate \"Scene 2\".\nThe remaining times \"Scene 3\" will be activated.\n\nClick the highlighted plus symbol below to add a new scene group."
AdvSceneSwitcher.sceneGroupTab.scenes.help="Select the scene group you want to modify on the left.\n\nSelect a scene to add to this scene group by selecting the scene above and clicking the plus symbol below.\n\nA scene can be added multiple times to the same scene group."
AdvSceneSwitcher.kwin.flatpakPermissionWarning="Window detection via KWin is unavailable.\nThe OBS Studio Flatpak sandbox is missing the 'org.kde.KWin' D-Bus permission this feature needs.\nRun the following, then restart OBS:"
AdvSceneSwitcher.wayland.flatpakPermissionWarning="Window detection is unavailable.\nThe OBS Studio Flatpak has no X11 access on this Wayland session, and no compatible integration exists yet for this desktop.\nRunning OBS under an X11 session, or a non-Flatpak install, allows this feature to work."
AdvSceneSwitcher.flatpakWindowDetectionWarning.doNotShowAgain="Don't show this warning again"

View File

@@ -1,16 +1,5 @@
#include "platform-funcs.hpp"
#include "log-helper.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs-frontend-api.h>
#include <QCheckBox>
#include <QFontDatabase>
#include <QGridLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
@@ -639,108 +628,27 @@ int ignoreXerror(Display *d, XErrorEvent *e)
return 0;
}
static bool warnAboutWindowDetectionUnavailable = true;
static bool setupWindowDetectionWarningPersistence = []() {
AddSaveStep([](obs_data_t *obj) {
obs_data_set_bool(obj, "warnAboutFlatpakWindowDetection",
warnAboutWindowDetectionUnavailable);
});
AddLoadStep([](obs_data_t *obj) {
obs_data_set_default_bool(
obj, "warnAboutFlatpakWindowDetection", true);
warnAboutWindowDetectionUnavailable = obs_data_get_bool(
obj, "warnAboutFlatpakWindowDetection");
});
return true;
}();
static void showWindowDetectionWarning(const char *messageId,
const char *command)
{
auto mainWindow =
static_cast<QWidget *>(obs_frontend_get_main_window());
QMessageBox msgBox(QMessageBox::Warning,
obs_module_text("AdvSceneSwitcher.pluginName"),
obs_module_text(messageId), QMessageBox::Ok,
mainWindow);
if (command) {
auto commandLineEdit = new QLineEdit(command, &msgBox);
commandLineEdit->setReadOnly(true);
commandLineEdit->setFont(
QFontDatabase::systemFont(QFontDatabase::FixedFont));
commandLineEdit->setCursorPosition(0);
auto layout = qobject_cast<QGridLayout *>(msgBox.layout());
if (layout) {
layout->addWidget(commandLineEdit, layout->rowCount(),
0, 1, layout->columnCount());
}
}
auto checkbox = new QCheckBox(obs_module_text(
"AdvSceneSwitcher.flatpakWindowDetectionWarning.doNotShowAgain"));
msgBox.setCheckBox(checkbox);
msgBox.exec();
warnAboutWindowDetectionUnavailable = !checkbox->isChecked();
}
static void handleWindowDetectionUnavailable(bool hasX11)
{
if (hasX11 || KWin) {
return;
}
if (!qEnvironmentVariableIsSet("FLATPAK_ID")) {
return;
}
const bool onKDE = qEnvironmentVariable("XDG_CURRENT_DESKTOP")
.contains("KDE", Qt::CaseInsensitive);
const char *messageId =
onKDE ? "AdvSceneSwitcher.kwin.flatpakPermissionWarning"
: "AdvSceneSwitcher.wayland.flatpakPermissionWarning";
const char *command =
onKDE ? "flatpak override --user --talk-name=org.kde.KWin com.obsproject.Studio"
: nullptr;
blog(LOG_WARNING,
"window detection unavailable: OBS Flatpak sandbox has no X11 "
"access on this Wayland session and %s",
onKDE ? "the KWin D-Bus compat call was blocked (missing "
"'org.kde.KWin' talk-name permission)"
: "no compatible native Wayland integration exists for "
"this desktop");
AddFinishedLoadingStep([messageId, command]() {
if (!warnAboutWindowDetectionUnavailable) {
return;
}
showWindowDetectionWarning(messageId, command);
});
}
void PlatformInit()
{
const bool kwinAvailable = isKWinAvailable();
KWin = kwinAvailable && registerKWinDBusListener(&notifier) &&
startKWinScript(KWinScriptObjectPath);
if (KWin) {
blog(LOG_INFO, "using KWin compat");
} else {
blog(LOG_INFO, "not using KWin compat");
}
initProcps();
initProc2();
auto display = disp();
handleWindowDetectionUnavailable(display != nullptr);
if (!display) {
return;
}
KWin = isKWinAvailable();
if (!(KWin && startKWinScript(KWinScriptObjectPath) &&
registerKWinDBusListener(&notifier))) {
// something bad happened while trying to initialize
// the KWin script/dbus so disable it
KWin = false;
blog(LOG_INFO, "not using KWin compat");
} else {
blog(LOG_INFO, "using KWin compat");
}
initXss();
initProcps();
initProc2();
XSetErrorHandler(ignoreXerror);
}

View File

@@ -5,7 +5,6 @@
#include <QDir>
#include <QFile>
#include <QFileDevice>
#include <QStandardPaths>
#include <QTextStream>
#include <QtDBus/QDBusConnectionInterface>
#include <QtDBus/QDBusInterface>
@@ -97,16 +96,12 @@ bool isKWinAvailable()
bool startKWinScript(QString &scriptObjectPath)
{
// Not RuntimeLocation: under Flatpak, $XDG_RUNTIME_DIR is private to
// the sandbox, so the unsandboxed KWin process can't read it back.
const QString scriptPath =
QStandardPaths::writableLocation(
QStandardPaths::CacheLocation) +
"/AdvancedSceneSwitcher/KWinFocusNotifier.js";
"/tmp/AdvancedSceneSwitcher/KWinFocusNotifier.js";
const QString script =
R"(var adss = "com.obsproject.Studio.Plugin.SceneSwitcher";
var adssPath = "/com/obsproject/Studio/Plugin/SceneSwitcher";
R"(var adss = "com.github.AdvancedSceneSwitcher";
var adssPath = "/com/github/AdvancedSceneSwitcher";
function trackWindow(window) {
var id = window.internalId.toString();
@@ -134,8 +129,7 @@ workspace.windowActivated.connect(function(client) {
}))";
if (const QDir dir; !dir.mkpath(QFileInfo(scriptPath).absolutePath())) {
blog(LOG_ERROR,
"error creating KWinFocusNotifier script directory");
blog(LOG_ERROR, "error creating /tmp/AdvancedSceneSwitcher");
return false;
}
@@ -204,10 +198,8 @@ bool stopKWinScript(const QString &scriptObjectPath)
bool registerKWinDBusListener(FocusNotifier *notifier)
{
static const QString serviceName =
"com.obsproject.Studio.Plugin.SceneSwitcher";
static const QString objectPath =
"/com/obsproject/Studio/Plugin/SceneSwitcher";
static const QString serviceName = "com.github.AdvancedSceneSwitcher";
static const QString objectPath = "/com/github/AdvancedSceneSwitcher";
auto bus = QDBusConnection::sessionBus();
if (bus.objectRegisteredAt(objectPath)) {

View File

@@ -12,8 +12,7 @@ namespace advss {
class FocusNotifier final : public QObject {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface",
"com.obsproject.Studio.Plugin.SceneSwitcher")
Q_CLASSINFO("D-Bus Interface", "com.github.AdvancedSceneSwitcher")
static std::mutex _mutex;
static int activePID;

View File

@@ -1,5 +1,6 @@
#include "macro-settings.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "macro.hpp"
#include "obs-module-helper.hpp"
#include "plugin-state-helpers.hpp"
@@ -8,11 +9,48 @@
#include <QScrollArea>
#include <QScrollBar>
#include <QVBoxLayout>
#include <set>
namespace advss {
static GlobalMacroSettings macroSettings;
static constexpr int addNewDockWindowRole = Qt::UserRole + 1;
static QStringList GetExistingDockWindowNames()
{
std::set<std::string> names;
for (const auto &macro : GetAllMacros()) {
if (!macro) {
continue;
}
const auto &dockSettings = macro->GetDockSettings();
if (dockSettings.IsStandaloneDock()) {
continue;
}
names.insert(dockSettings.DockWindowName());
}
QStringList result;
for (const auto &name : names) {
result << QString::fromStdString(name);
}
return result;
}
static void populateDockWindowNameCombo(QComboBox *combo)
{
combo->clear();
auto names = GetExistingDockWindowNames();
combo->addItems(names);
if (!names.isEmpty()) {
combo->insertSeparator(combo->count());
}
combo->addItem(obs_module_text(
"AdvSceneSwitcher.macroTab.currentDockWindowName.addNew"));
combo->setItemData(combo->count() - 1, true, addNewDockWindowRole);
}
void GlobalMacroSettings::Save(obs_data_t *obj) const
{
auto data = obs_data_create();
@@ -96,7 +134,7 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
"AdvSceneSwitcher.macroTab.currentRegisterDock"))),
_currentMacroIsStandaloneDock(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.macroTab.currentIsStandaloneDock"))),
_currentMacroDockWindowName(new QLineEdit(this)),
_currentMacroDockWindowName(new QComboBox(this)),
_currentMacroDockAddRunButton(new QCheckBox(obs_module_text(
"AdvSceneSwitcher.macroTab.currentDockAddRunButton"))),
_currentMacroDockAddPauseButton(new QCheckBox(obs_module_text(
@@ -143,6 +181,17 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
"AdvSceneSwitcher.macroTab.pauseStateSaveBehavior.unpause"),
static_cast<int>(Macro::PauseStateSaveBehavior::UNPAUSE));
_currentMacroDockWindowName->setEditable(true);
populateDockWindowNameCombo(_currentMacroDockWindowName);
connect(_currentMacroDockWindowName,
qOverload<int>(&QComboBox::activated), this, [this](int index) {
if (_currentMacroDockWindowName
->itemData(index, addNewDockWindowRole)
.toBool()) {
_currentMacroDockWindowName->setCurrentText("");
}
});
auto highlightOptions = new QGroupBox(
obs_module_text("AdvSceneSwitcher.macroTab.highlightSettings"));
auto highlightLayout = new QVBoxLayout;
@@ -348,7 +397,7 @@ MacroSettingsDialog::MacroSettingsDialog(QWidget *parent,
_currentMacroRegisterDock->setChecked(dockEnabled);
_currentMacroIsStandaloneDock->setChecked(
dockSettings.IsStandaloneDock());
_currentMacroDockWindowName->setText(
_currentMacroDockWindowName->setCurrentText(
QString::fromStdString(dockSettings.DockWindowName()));
_currentMacroDockAddRunButton->setChecked(dockSettings.HasRunButton());
_currentMacroDockAddPauseButton->setChecked(
@@ -486,7 +535,7 @@ bool MacroSettingsDialog::AskForSettings(QWidget *parent,
dockSettings.SetIsStandaloneDock(
dialog._currentMacroIsStandaloneDock->isChecked());
dockSettings.SetDockWindowName(
dialog._currentMacroDockWindowName->text().toStdString());
dialog._currentMacroDockWindowName->currentText().toStdString());
dockSettings.SetHasRunButton(
dialog._currentMacroDockAddRunButton->isChecked());
dockSettings.SetHasPauseButton(

View File

@@ -6,6 +6,7 @@
#include <QWidget>
#include <QDialog>
#include <QCheckBox>
#include <QComboBox>
#include <QGroupBox>
#include <QLineEdit>
#include <QGridLayout>
@@ -71,7 +72,7 @@ private:
MacroInputSelection *_currentInputs;
QCheckBox *_currentMacroRegisterDock;
QCheckBox *_currentMacroIsStandaloneDock;
QLineEdit *_currentMacroDockWindowName;
QComboBox *_currentMacroDockWindowName;
QCheckBox *_currentMacroDockAddRunButton;
QCheckBox *_currentMacroDockAddPauseButton;
QCheckBox *_currentMacroDockAddStatusLabel;

View File

@@ -2,13 +2,16 @@
#include "obs-module-helper.hpp"
#include <climits>
#ifdef EXPRTK_SUPPORT
#include <exprtk.hpp>
#include <random>
#endif
namespace advss {
std::variant<double, std::string> EvalMathExpression(const std::string &expr)
{
#ifdef EXPRTK_SUPPORT
static auto randomFunc = []() {
thread_local std::mt19937 gen(std::random_device{}());
thread_local std::uniform_real_distribution<double> dis(0.0,
@@ -26,6 +29,7 @@ std::variant<double, std::string> EvalMathExpression(const std::string &expr)
if (parser.compile(expr, expression)) {
return expression.value();
}
#endif
return std::string(obs_module_text(
"AdvSceneSwitcher.math.expressionFail")) +
" \"" + expr + "\"";

View File

@@ -127,9 +127,10 @@ void RegisterWebsocketRequest(
void SendWebsocketVendorEvent(const std::string &eventName, obs_data_t *data)
{
if (OBSIsShuttingDown()) {
if (OBSIsShuttingDown() || !obs_get_module("obs-websocket")) {
return;
}
obs_websocket_vendor_emit_event(vendor, eventName.c_str(), data);
}

View File

@@ -2,18 +2,64 @@
#include "audio-helpers.hpp"
#include "layout-helpers.hpp"
#include "macro-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "sync-helpers.hpp"
#include <obs-frontend-api.h>
#include <chrono>
#include <mutex>
#include <set>
#include <QFileInfo>
#include <QLabel>
namespace advss {
// Use a high output channel index that is unlikely to be claimed by OBS or
// other plugins. OBS supports channels 0-63; channel 0 is the main scene.
static constexpr uint32_t kPlaybackOutputChannel = 63;
// Tracks rawSource handles held by background cleanup threads so they can be
// stopped early when OBS begins shutdown, before audio is freed.
static std::mutex g_activeSourcesMutex;
static std::set<obs_source_t *> g_activeSources;
static void registerActiveSource(obs_source_t *source)
{
std::lock_guard<std::mutex> lock(g_activeSourcesMutex);
g_activeSources.insert(source);
}
static void unregisterActiveSource(obs_source_t *source)
{
std::lock_guard<std::mutex> lock(g_activeSourcesMutex);
g_activeSources.erase(source);
}
static void stopAllActiveSources()
{
std::lock_guard<std::mutex> lock(g_activeSourcesMutex);
for (auto *source : g_activeSources) {
obs_source_media_stop(source);
}
}
static void handleObsEvent(enum obs_frontend_event event, void *)
{
if (event == OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN ||
event == OBS_FRONTEND_EVENT_EXIT) {
stopAllActiveSources();
}
}
static bool setup()
{
AddPluginPostLoadStep([]() {
obs_frontend_add_event_callback(handleObsEvent, nullptr);
});
return true;
}
static bool setupDone = setup();
const std::string MacroActionPlayAudio::id = "play_audio";
bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
@@ -23,6 +69,7 @@ bool MacroActionPlayAudio::_registered = MacroActionFactory::Register(
static void deactivatePlayback(obs_source_t *source, bool wantsOutput)
{
obs_source_media_stop(source);
if (wantsOutput) {
obs_set_output_source(kPlaybackOutputChannel, nullptr);
} else {
@@ -38,8 +85,7 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
std::unique_lock<std::mutex> lock(*GetMutex());
SetMacroAbortWait(false);
// The media source needs time to open and decode before reaching
// PLAYING state. Poll until it starts (or the macro is stopped).
// Poll until the source starts playing or the macro is stopped.
while (!MacroWaitShouldAbort() && !MacroIsStopped(macro)) {
if (obs_source_media_get_state(source) ==
OBS_MEDIA_STATE_PLAYING) {
@@ -48,9 +94,8 @@ static void waitForPlaybackToEnd(Macro *macro, obs_source_t *source,
GetMacroWaitCV().wait_for(lock, 10ms);
}
// Now wait for playback to end. Require two consecutive non-playing
// samples to avoid false positives on brief state transitions.
// If maxMs > 0, also stop once that many milliseconds have elapsed.
// Wait for playback to end. Two consecutive non-playing samples are
// required to avoid false positives on brief state transitions.
const auto playbackStart = std::chrono::steady_clock::now();
static const int kStopThreshold = 2;
int stoppedCount = 0;
@@ -95,7 +140,6 @@ bool MacroActionPlayAudio::PerformAction()
obs_data_set_string(settings, "local_file", path.c_str());
obs_data_set_bool(settings, "is_local_file", true);
obs_data_set_bool(settings, "looping", false);
// Disable automatic restart on activate so we control start explicitly.
obs_data_set_bool(settings, "restart_on_activate", false);
obs_data_set_bool(settings, "close_when_inactive", true);
@@ -113,10 +157,11 @@ bool MacroActionPlayAudio::PerformAction()
DecibelToPercent(static_cast<float>(_volumeDB.GetValue()));
obs_source_set_volume(source, vol);
obs_source_set_monitoring_type(source, _monitorType);
if (_mono) {
obs_source_set_flags(source, OBS_SOURCE_FLAG_FORCE_MONO);
}
// Fall back to monitor-only if all output tracks are deselected —
// there is no point routing through the output channel if the mixer
// mask would silence every track.
// Fall back to monitor-only if all output tracks are deselected.
const bool wantsOutput =
(_monitorType != OBS_MONITORING_TYPE_MONITOR_ONLY) &&
(_audioMixers != 0);
@@ -128,10 +173,8 @@ bool MacroActionPlayAudio::PerformAction()
if (wantsOutput) {
obs_source_set_audio_mixers(source, _audioMixers);
// Route through a private scene so we can position the scene
// item far off-screen. This keeps the item "visible" (so audio
// is still mixed) while ensuring its video never intersects the
// output frame.
// Route through a private scene positioned off-screen so the
// audio mixes into the output without video appearing on screen.
OBSSceneAutoRelease audioScene =
obs_scene_create_private("advss_audio_scene");
obs_sceneitem_t *item = obs_scene_add(audioScene, source);
@@ -141,8 +184,7 @@ bool MacroActionPlayAudio::PerformAction()
}
obs_set_output_source(kPlaybackOutputChannel,
obs_scene_get_source(audioScene));
// audioScene released here; the output channel holds the
// remaining reference and keeps the scene alive.
// audioScene is released here; the output channel keeps the scene alive.
} else {
obs_source_set_audio_mixers(source, 0);
obs_source_inc_active(source);
@@ -167,15 +209,15 @@ bool MacroActionPlayAudio::PerformAction()
return true;
}
// Keep the source alive in a background thread that cleans up
// once playback finishes. Grab an extra strong reference so the
// source survives beyond this stack frame.
// Keep the source alive until playback ends via a background thread.
auto rawSource = obs_source_get_ref(source);
registerActiveSource(rawSource);
auto macro = GetMacro();
std::thread cleanupThread([rawSource, wantsOutput, macro, maxMs]() {
waitForPlaybackToEnd(macro, rawSource, maxMs);
deactivatePlayback(rawSource, wantsOutput);
unregisterActiveSource(rawSource);
obs_source_release(rawSource);
});
AddMacroHelperThread(macro, std::move(cleanupThread));
@@ -204,6 +246,7 @@ bool MacroActionPlayAudio::Save(obs_data_t *obj) const
obs_data_set_bool(obj, "useDuration", _useDuration);
_playbackDuration.Save(obj, "playbackDuration");
obs_data_set_bool(obj, "waitForCompletion", _waitForCompletion);
obs_data_set_bool(obj, "mono", _mono);
return true;
}
@@ -221,6 +264,7 @@ bool MacroActionPlayAudio::Load(obs_data_t *obj)
_useDuration = obs_data_get_bool(obj, "useDuration");
_playbackDuration.Load(obj, "playbackDuration");
_waitForCompletion = obs_data_get_bool(obj, "waitForCompletion");
_mono = obs_data_get_bool(obj, "mono");
return true;
}
@@ -262,7 +306,9 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
_useDuration(new QCheckBox(this)),
_playbackDuration(new DurationSelection(this, true, 0.0)),
_waitForCompletion(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.playAudio.wait")))
obs_module_text("AdvSceneSwitcher.action.playAudio.wait"))),
_mono(new QCheckBox(
obs_module_text("AdvSceneSwitcher.action.playAudio.mono")))
{
_volumeDB->setMinimum(-100.0);
_volumeDB->setMaximum(0.0);
@@ -303,6 +349,8 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
SLOT(PlaybackDurationChanged(const Duration &)));
QWidget::connect(_waitForCompletion, SIGNAL(stateChanged(int)), this,
SLOT(WaitChanged(int)));
QWidget::connect(_mono, SIGNAL(stateChanged(int)), this,
SLOT(MonoChanged(int)));
auto tracksLayout = new QHBoxLayout;
tracksLayout->setContentsMargins(0, 0, 0, 0);
@@ -346,6 +394,7 @@ MacroActionPlayAudioEdit::MacroActionPlayAudioEdit(
mainLayout->addLayout(startOffsetLayout);
mainLayout->addLayout(playbackDurationLayout);
mainLayout->addWidget(_waitForCompletion);
mainLayout->addWidget(_mono);
setLayout(mainLayout);
_entryData = entryData;
@@ -375,6 +424,7 @@ void MacroActionPlayAudioEdit::UpdateEntryData()
_playbackDuration->SetDuration(_entryData->_playbackDuration);
_playbackDuration->setEnabled(_entryData->_useDuration);
_waitForCompletion->setChecked(_entryData->_waitForCompletion);
_mono->setChecked(_entryData->_mono);
}
void MacroActionPlayAudioEdit::FilePathChanged(const QString &path)
@@ -443,4 +493,10 @@ void MacroActionPlayAudioEdit::WaitChanged(int value)
_entryData->_waitForCompletion = value;
}
void MacroActionPlayAudioEdit::MonoChanged(int value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_mono = value;
}
} // namespace advss

View File

@@ -37,6 +37,7 @@ public:
bool _useDuration = false;
Duration _playbackDuration;
bool _waitForCompletion = false;
bool _mono = false;
static bool _registered;
static const std::string id;
@@ -68,6 +69,7 @@ private slots:
void UseDurationChanged(int);
void PlaybackDurationChanged(const Duration &);
void WaitChanged(int value);
void MonoChanged(int value);
signals:
void HeaderInfoChanged(const QString &);
@@ -83,6 +85,7 @@ private:
QCheckBox *_useDuration;
DurationSelection *_playbackDuration;
QCheckBox *_waitForCompletion;
QCheckBox *_mono;
std::shared_ptr<MacroActionPlayAudio> _entryData;
bool _loading = true;

View File

@@ -1,7 +1,9 @@
#include "macro-action-scene-collection.hpp"
#include "layout-helpers.hpp"
#include "plugin-state-helpers.hpp"
#include "selection-helpers.hpp"
#include "ui-helpers.hpp"
#include <obs-frontend-api.h>
@@ -15,6 +17,20 @@ bool MacroActionSceneCollection::_registered = MacroActionFactory::Register(
MacroActionSceneCollectionEdit::Create,
"AdvSceneSwitcher.action.sceneCollection"});
template<typename F> void QueueUITaskLambda(F &&func)
{
using FnType = std::decay_t<F>;
auto *heapFunc = new FnType(std::forward<F>(func));
QueueUITask(
[](void *param) {
std::unique_ptr<FnType> fn(
static_cast<FnType *>(param));
(*fn)();
},
heapFunc);
}
bool MacroActionSceneCollection::PerformAction()
{
// Changing the scene collection will also reload the settings of the
@@ -23,7 +39,13 @@ bool MacroActionSceneCollection::PerformAction()
if (SettingsWindowIsOpened()) {
return false;
}
obs_frontend_set_current_scene_collection(_sceneCollection.c_str());
const auto collectionName = _sceneCollection;
QueueUITaskLambda([collectionName]() {
obs_frontend_set_current_scene_collection(
collectionName.c_str());
});
// It does not make sense to continue as the current settings will be
// invalid after switching scene collection.
return false;

View File

@@ -25,6 +25,8 @@ const static std::map<MacroConditionAudio::Type, std::string> checkTypes = {
"AdvSceneSwitcher.condition.audio.type.monitor"},
{MacroConditionAudio::Type::BALANCE,
"AdvSceneSwitcher.condition.audio.type.balance"},
{MacroConditionAudio::Type::TRACK,
"AdvSceneSwitcher.condition.audio.type.track"},
};
const static std::map<MacroConditionAudio::OutputCondition, std::string>
@@ -245,6 +247,20 @@ bool MacroConditionAudio::CheckBalance()
return ret && source;
}
bool MacroConditionAudio::CheckTrack()
{
if (!_audioSource.GetSource()) {
return false;
}
OBSSourceAutoRelease source =
obs_weak_source_get_source(_audioSource.GetSource());
uint32_t mixers = obs_source_get_audio_mixers(source);
bool enabled = (mixers >> (uint32_t)(_track - 1)) & 1u;
SetTempVarValue("track_enabled", enabled ? "true" : "false");
return enabled && source;
}
bool MacroConditionAudio::CheckCondition()
{
bool ret = false;
@@ -264,6 +280,9 @@ bool MacroConditionAudio::CheckCondition()
case Type::BALANCE:
ret = CheckBalance();
break;
case Type::TRACK:
ret = CheckTrack();
break;
}
if (GetVariableValue().empty()) {
@@ -281,6 +300,7 @@ bool MacroConditionAudio::Save(obs_data_t *obj) const
_volumePercent.Save(obj, "volume");
_syncOffset.Save(obj, "syncOffset");
_balance.Save(obj, "balance");
_track.Save(obj, "track");
obs_data_set_int(obj, "checkType", static_cast<int>(_checkType));
obs_data_set_int(obj, "outputCondition",
static_cast<int>(_outputCondition));
@@ -346,6 +366,7 @@ bool MacroConditionAudio::Load(obs_data_t *obj)
_useDb = obs_data_get_bool(obj, "useDb");
_volumeDB.Load(obj, "volumeDB");
}
_track.Load(obj, "track");
return true;
}
@@ -421,6 +442,12 @@ void MacroConditionAudio::SetupTempVars()
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.balance"));
break;
case Type::TRACK:
AddTempvar(
"track_enabled",
obs_module_text(
"AdvSceneSwitcher.tempVar.audio.trackEnabled"));
break;
default:
break;
}
@@ -468,6 +495,7 @@ static QStringList getAudioSourcesList()
MacroConditionAudioEdit::MacroConditionAudioEdit(
QWidget *parent, std::shared_ptr<MacroConditionAudio> entryData)
: QWidget(parent),
_layout(new QHBoxLayout),
_checkTypes(new QComboBox()),
_sources(new SourceSelectionWidget(this, getAudioSourcesList, true)),
_condition(new QComboBox()),
@@ -476,7 +504,8 @@ MacroConditionAudioEdit::MacroConditionAudioEdit(
_percentDBToggle(new QPushButton),
_syncOffset(new VariableSpinBox()),
_monitorTypes(new QComboBox),
_balance(new SliderSpinBox(0., 1., ""))
_balance(new SliderSpinBox(0., 1., "")),
_track(new VariableSpinBox())
{
_volumePercent->setSuffix("%");
_volumePercent->setMaximum(100);
@@ -491,6 +520,9 @@ MacroConditionAudioEdit::MacroConditionAudioEdit(
_syncOffset->setMaximum(20000);
_syncOffset->setSuffix("ms");
_track->setMinimum(1);
_track->setMaximum(32);
QWidget::connect(_checkTypes, SIGNAL(currentIndexChanged(int)), this,
SLOT(CheckTypeChanged(int)));
QWidget::connect(
@@ -519,27 +551,16 @@ MacroConditionAudioEdit::MacroConditionAudioEdit(
this, SLOT(VolumeDBChanged(const NumberVariable<double> &)));
QWidget::connect(_percentDBToggle, SIGNAL(clicked()), this,
SLOT(PercentDBClicked()));
QWidget::connect(
_track,
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
this, SLOT(TrackChanged(const NumberVariable<int> &)));
populateCheckTypes(_checkTypes);
PopulateMonitorTypeSelection(_monitorTypes);
QHBoxLayout *switchLayout = new QHBoxLayout;
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
{"{{checkType}}", _checkTypes},
{"{{audioSources}}", _sources},
{"{{volume}}", _volumePercent},
{"{{syncOffset}}", _syncOffset},
{"{{monitorTypes}}", _monitorTypes},
{"{{balance}}", _balance},
{"{{condition}}", _condition},
{"{{volumeDB}}", _volumeDB},
{"{{percentDBToggle}}", _percentDBToggle},
};
PlaceWidgets(obs_module_text("AdvSceneSwitcher.condition.audio.entry"),
switchLayout, widgetPlaceholders);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addLayout(switchLayout);
mainLayout->addLayout(_layout);
mainLayout->addWidget(_balance);
setLayout(mainLayout);
@@ -608,6 +629,12 @@ void MacroConditionAudioEdit::BalanceChanged(const NumberVariable<double> &value
_entryData->_balance = value;
}
void MacroConditionAudioEdit::TrackChanged(const NumberVariable<int> &value)
{
GUARD_LOADING_AND_LOCK();
_entryData->_track = value;
}
void MacroConditionAudioEdit::VolumeDBChanged(
const NumberVariable<double> &value)
{
@@ -692,6 +719,8 @@ void MacroConditionAudioEdit::CheckTypeChanged(int idx)
} else if (_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME) {
populateVolumeConditionSelection(_condition);
} else if (_entryData->GetType() == MacroConditionAudio::Type::TRACK) {
_condition->clear();
}
SetWidgetVisibility();
}
@@ -708,6 +737,7 @@ void MacroConditionAudioEdit::UpdateEntryData()
_syncOffset->SetValue(_entryData->_syncOffset);
_monitorTypes->setCurrentIndex(_entryData->_monitorType);
_balance->SetDoubleValue(_entryData->_balance);
_track->SetValue(_entryData->_track);
_checkTypes->setCurrentIndex(
_checkTypes->findData(static_cast<int>(_entryData->GetType())));
@@ -748,26 +778,63 @@ void MacroConditionAudioEdit::SetWidgetVisibility()
return;
}
_layout->removeWidget(_checkTypes);
_layout->removeWidget(_sources);
_layout->removeWidget(_condition);
_layout->removeWidget(_volumePercent);
_layout->removeWidget(_volumeDB);
_layout->removeWidget(_percentDBToggle);
_layout->removeWidget(_syncOffset);
_layout->removeWidget(_monitorTypes);
_layout->removeWidget(_track);
ClearLayout(_layout);
const std::unordered_map<std::string, QWidget *> placeholders = {
{"{{checkType}}", _checkTypes},
{"{{audioSources}}", _sources},
{"{{condition}}", _condition},
{"{{volume}}", _volumePercent},
{"{{volumeDB}}", _volumeDB},
{"{{percentDBToggle}}", _percentDBToggle},
{"{{syncOffset}}", _syncOffset},
{"{{monitorTypes}}", _monitorTypes},
{"{{track}}", _track},
};
const bool isTrack = _entryData->GetType() ==
MacroConditionAudio::Type::TRACK;
PlaceWidgets(
obs_module_text(
isTrack ? "AdvSceneSwitcher.condition.audio.layout.track"
: "AdvSceneSwitcher.condition.audio.entry"),
_layout, placeholders);
_condition->setVisible(
_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME ||
_entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET);
_syncOffset->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET);
_monitorTypes->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::MONITOR);
!isTrack &&
(_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME ||
_entryData->GetType() ==
MacroConditionAudio::Type::CONFIGURED_VOLUME ||
_entryData->GetType() == MacroConditionAudio::Type::BALANCE ||
_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET));
_syncOffset->setVisible(!isTrack &&
_entryData->GetType() ==
MacroConditionAudio::Type::SYNC_OFFSET);
_monitorTypes->setVisible(!isTrack &&
_entryData->GetType() ==
MacroConditionAudio::Type::MONITOR);
_track->setVisible(isTrack);
_volumePercent->setVisible(!isTrack && HasVolumeControl() &&
!_entryData->_useDb);
_volumeDB->setVisible(!isTrack && HasVolumeControl() &&
_entryData->_useDb);
_percentDBToggle->setText(_entryData->_useDb ? "dB" : "%");
_percentDBToggle->setVisible(!isTrack && HasVolumeControl());
_balance->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::BALANCE);
_volMeter->setVisible(_entryData->GetType() ==
MacroConditionAudio::Type::OUTPUT_VOLUME);
_volumePercent->setVisible(HasVolumeControl() && !_entryData->_useDb);
_volumeDB->setVisible(HasVolumeControl() && _entryData->_useDb);
_percentDBToggle->setText(_entryData->_useDb ? "dB" : "%");
_percentDBToggle->setVisible(HasVolumeControl());
adjustSize();
}

View File

@@ -5,6 +5,7 @@
#include "source-selection.hpp"
#include <limits>
#include <QHBoxLayout>
#include <QWidget>
#include <QComboBox>
#include <chrono>
@@ -36,6 +37,7 @@ public:
SYNC_OFFSET,
MONITOR,
BALANCE,
TRACK,
};
void SetType(const Type &);
Type GetType() const { return _checkType; }
@@ -60,6 +62,7 @@ public:
IntVariable _syncOffset = 0;
obs_monitoring_type _monitorType = OBS_MONITORING_TYPE_NONE;
DoubleVariable _balance = 0.5;
IntVariable _track = 1;
OutputCondition _outputCondition = OutputCondition::ABOVE;
VolumeCondition _volumeCondition = VolumeCondition::ABOVE;
obs_volmeter_t *_volmeter = nullptr;
@@ -70,6 +73,7 @@ private:
bool CheckSyncOffset();
bool CheckMonitor();
bool CheckBalance();
bool CheckTrack();
void SetupTempVars();
float GetVolumePeak();
@@ -111,6 +115,7 @@ private slots:
void VolumeDBChanged(const NumberVariable<double> &value);
void PercentDBClicked();
void SyncSliderAndValueSelection(bool sliderMoved);
void TrackChanged(const NumberVariable<int> &value);
signals:
void HeaderInfoChanged(const QString &);
@@ -119,6 +124,7 @@ private:
void SetWidgetVisibility();
bool HasVolumeControl() const;
QHBoxLayout *_layout;
QComboBox *_checkTypes;
SourceSelectionWidget *_sources;
QComboBox *_condition;
@@ -128,6 +134,7 @@ private:
VariableSpinBox *_syncOffset;
QComboBox *_monitorTypes;
SliderSpinBox *_balance;
VariableSpinBox *_track;
VolControl *_volMeter = nullptr;
std::shared_ptr<MacroConditionAudio> _entryData;

View File

@@ -233,7 +233,12 @@ void TwitchToken::Load(obs_data_t *obj)
{
Item::Load(obj);
_token = obs_data_get_string(obj, "token");
_userID = obs_data_get_string(obj, "userID");
if (obs_data_has_user_value(obj, "userID") &&
*obs_data_get_string(obj, "userID") != '\0') {
_userID = obs_data_get_string(obj, "userID");
} else {
_userID = {};
}
obs_data_set_default_bool(obj, "validateEventSubTimestamps", true);
_validateEventSubTimestamps =
obs_data_get_bool(obj, "validateEventSubTimestamps");
@@ -255,7 +260,7 @@ void TwitchToken::Save(obs_data_t *obj) const
{
Item::Save(obj);
obs_data_set_string(obj, "token", _token.c_str());
if (_userID) {
if (_userID && !_userID->empty()) {
obs_data_set_string(obj, "userID", _userID->c_str());
}
obs_data_set_bool(obj, "validateEventSubTimestamps",
@@ -310,7 +315,7 @@ bool TwitchToken::AnyOptionIsEnabled(
return false;
}
void TwitchToken::SetToken(const std::string &value)
bool TwitchToken::SetToken(const std::string &value)
{
_token = value;
auto res =
@@ -318,21 +323,34 @@ void TwitchToken::SetToken(const std::string &value)
if (res.status != 200) {
blog(LOG_WARNING, "failed to get Twitch user id from token!");
_userID = {};
return;
return false;
}
bool gotUserID = false;
OBSDataArrayAutoRelease array = obs_data_get_array(res.data, "data");
size_t count = obs_data_array_count(array);
for (size_t i = 0; i < count; i++) {
OBSDataAutoRelease arrayObj = obs_data_array_item(array, i);
_userID = obs_data_get_string(arrayObj, "id");
std::string id = obs_data_get_string(arrayObj, "id");
if (id.empty()) {
blog(LOG_WARNING,
"Twitch /helix/users response did not contain a user id!");
continue;
}
_userID = id;
_name = obs_data_get_string(arrayObj, "display_name");
gotUserID = true;
}
if (!gotUserID) {
return false;
}
// Trigger resubscribes with new token
if (_eventSub) {
_eventSub->ClearActiveSubscriptions();
}
return true;
}
std::optional<std::string> TwitchToken::GetToken() const
@@ -388,7 +406,7 @@ bool TwitchToken::IsValid(bool forceUpdate) const
return false;
}
if (_userID && _userID != id) {
if (_userID && !_userID->empty() && _userID != id) {
blog(LOG_INFO,
"Twitch token %s does not match expected user (got %s, expected %s)!",
_name.c_str(), id, _userID->c_str());
@@ -396,6 +414,10 @@ bool TwitchToken::IsValid(bool forceUpdate) const
return false;
}
if (!_userID || _userID->empty()) {
_userID = id;
}
_lastValidityCheckResult = true;
return true;
};
@@ -819,9 +841,9 @@ void TwitchTokenSettingsDialog::GotToken(const std::optional<QString> &value)
return;
}
_currentToken.SetToken(value.value().toStdString());
bool success = _currentToken.SetToken(value.value().toStdString());
auto name = QString::fromStdString(_currentToken._name);
if (name.isEmpty()) {
if (!success || name.isEmpty()) {
_tokenStatus->setText(obs_module_text(
"AdvSceneSwitcher.twitchToken.request.fail"));
_name->setText("");

View File

@@ -48,7 +48,7 @@ public:
bool OptionIsActive(const TokenOption &option) const;
bool OptionIsEnabled(const TokenOption &option) const;
bool AnyOptionIsEnabled(const std::vector<TokenOption> &options) const;
void SetToken(const std::string &);
bool SetToken(const std::string &);
bool IsEmpty() const { return _token.empty(); }
std::optional<std::string> GetToken() const;
std::optional<std::string> GetUserID() const { return _userID; }
@@ -65,7 +65,7 @@ private:
mutable std::string _lastValidityCheckValue;
mutable bool _lastValidityCheckResult = false;
mutable std::chrono::system_clock::time_point _lastValidityCheckTime;
std::optional<std::string> _userID;
mutable std::optional<std::string> _userID;
std::set<TokenOption> _tokenOptions = TokenOption::GetAllTokenOptions();
std::shared_ptr<EventSub> _eventSub;
bool _validateEventSubTimestamps = false;

View File

@@ -85,8 +85,11 @@ target_sources(
${PROJECT_NAME}
PRIVATE test-math.cpp ${ADVSS_SOURCE_DIR}/lib/utils/math-helpers.cpp
${ADVSS_SOURCE_DIR}/lib/utils/obs-module-helper.cpp)
target_include_directories(${PROJECT_NAME}
PRIVATE ${ADVSS_SOURCE_DIR}/deps/exprtk)
if(EXISTS "${ADVSS_SOURCE_DIR}/deps/exprtk/exprtk.hpp")
target_include_directories(${PROJECT_NAME}
PRIVATE ${ADVSS_SOURCE_DIR}/deps/exprtk)
target_compile_definitions(${PROJECT_NAME} PRIVATE EXPRTK_SUPPORT=1)
endif()
if(MSVC)
target_compile_options(${PROJECT_NAME} PUBLIC /MP /d2FH4- /wd4267 /bigobj)