mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 12:45:28 -05:00
Compare commits
42 Commits
item-trans
...
1.36.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29e07ce63a | ||
|
|
5b8d829bba | ||
|
|
ad03b92d2d | ||
|
|
88fc412900 | ||
|
|
c2ec6848aa | ||
|
|
01da2877b0 | ||
|
|
107d77f1b8 | ||
|
|
1030868a75 | ||
|
|
797dc2ceb1 | ||
|
|
b971bfe800 | ||
|
|
f88407b731 | ||
|
|
725e3cacb8 | ||
|
|
c9eed5d997 | ||
|
|
bf682136c3 | ||
|
|
7be2a4f544 | ||
|
|
616664e6f3 | ||
|
|
0caf8628b7 | ||
|
|
463e7caa2c | ||
|
|
0e2315807f | ||
|
|
46bf6c666f | ||
|
|
902be7759d | ||
|
|
b4a1593dc9 | ||
|
|
577cfc6ba9 | ||
|
|
f7a29dfa69 | ||
|
|
e3475cf8ee | ||
|
|
b344f75a30 | ||
|
|
338a478ead | ||
|
|
2658ced18e | ||
|
|
cd6ff4d52b | ||
|
|
5438a6a364 | ||
|
|
ea1dea8621 | ||
|
|
3a556113aa | ||
|
|
eded0367db | ||
|
|
6c90f517e4 | ||
|
|
c07c00325f | ||
|
|
0a2e2cd771 | ||
|
|
8f3b5f9dfc | ||
|
|
78499bcf5f | ||
|
|
78fa776ce5 | ||
|
|
93c6415547 | ||
|
|
12f26c29e6 | ||
|
|
e4f6d94247 |
1
.github/scripts/.Aptfile
vendored
1
.github/scripts/.Aptfile
vendored
@@ -8,6 +8,7 @@ package 'libcurl4-openssl-dev'
|
||||
package 'libxtst-dev'
|
||||
package 'libxss-dev'
|
||||
package 'libopencv-dev'
|
||||
package 'libopencv-contrib-dev'
|
||||
package 'libtesseract-dev'
|
||||
package 'libproc2-dev'
|
||||
package 'libusb-1.0-0-dev'
|
||||
|
||||
58
.github/scripts/.build-deps.zsh
vendored
58
.github/scripts/.build-deps.zsh
vendored
@@ -284,30 +284,56 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
case ${host_os} {
|
||||
macos)
|
||||
local opencv_dir="${project_root}/deps/opencv"
|
||||
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
|
||||
local opencv_contrib_dir="${project_root}/deps/opencv_contrib"
|
||||
local opencv_install_x86="${opencv_dir}/install_x86_64"
|
||||
local opencv_install_arm="${opencv_dir}/install_arm64"
|
||||
|
||||
local -a opencv_cmake_args=(
|
||||
local -a opencv_cmake_args_common=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_LIST=core,imgproc,objdetect
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DBUILD_LIST=core,imgproc,objdetect,xobjdetect,dnn
|
||||
-DOPENCV_EXTRA_MODULES_PATH="${opencv_contrib_dir}/modules"
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DCMAKE_PREFIX_PATH="${advss_dep_path};${_plugin_deps}"
|
||||
-DCMAKE_INSTALL_PREFIX="${advss_dep_path}"
|
||||
-DWITH_KLEIDICV=OFF
|
||||
)
|
||||
|
||||
if [ "${target}" != "macos-x86_64" ]; then
|
||||
opencv_cmake_args+=(-DWITH_IPP=OFF)
|
||||
fi
|
||||
|
||||
pushd ${opencv_dir}
|
||||
log_info "Configure OpenCV ..."
|
||||
cmake -S . -B ${opencv_build_dir} ${opencv_cmake_args}
|
||||
|
||||
log_info "Building OpenCV ..."
|
||||
cmake --build ${opencv_build_dir} --config Release
|
||||
log_info "Configure OpenCV (x86_64) ..."
|
||||
cmake -S . -B build_x86_64 ${opencv_cmake_args_common} \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_PROJECT_INCLUDE="${SCRIPT_HOME}/opencv-force-processor-x86_64.cmake" \
|
||||
-DCMAKE_INSTALL_PREFIX="${opencv_install_x86}" \
|
||||
-DWITH_IPP=OFF
|
||||
|
||||
log_info "Installing OpenCV ..."
|
||||
cmake --install ${opencv_build_dir} --prefix "${advss_dep_path}" --config Release || true
|
||||
log_info "Building OpenCV (x86_64) ..."
|
||||
cmake --build build_x86_64 --config Release
|
||||
|
||||
log_info "Installing OpenCV (x86_64) ..."
|
||||
cmake --install build_x86_64 --prefix "${opencv_install_x86}" --config Release || true
|
||||
|
||||
log_info "Configure OpenCV (arm64) ..."
|
||||
cmake -S . -B build_arm64 ${opencv_cmake_args_common} \
|
||||
-DCMAKE_OSX_ARCHITECTURES=arm64 \
|
||||
-DCMAKE_INSTALL_PREFIX="${opencv_install_arm}"
|
||||
|
||||
log_info "Building OpenCV (arm64) ..."
|
||||
cmake --build build_arm64 --config Release
|
||||
|
||||
log_info "Installing OpenCV (arm64) ..."
|
||||
cmake --install build_arm64 --prefix "${opencv_install_arm}" --config Release || true
|
||||
|
||||
log_info "Merging OpenCV into universal binaries ..."
|
||||
cp -R "${opencv_install_arm}/." "${advss_dep_path}"
|
||||
for arm_lib in ${opencv_install_arm}/lib/**/*.(dylib|a)(.); do
|
||||
local rel="${arm_lib#${opencv_install_arm}/}"
|
||||
local x86_lib="${opencv_install_x86}/${rel}"
|
||||
if [[ -f "${x86_lib}" ]]; then
|
||||
lipo -create "${x86_lib}" "${arm_lib}" -output "${advss_dep_path}/${rel}"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf "${opencv_install_x86}" "${opencv_install_arm}"
|
||||
popd
|
||||
|
||||
local leptonica_dir="${project_root}/deps/leptonica"
|
||||
@@ -319,7 +345,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DSW_BUILD=OFF
|
||||
-DOPENJPEG_SUPPORT=OFF
|
||||
-DLIBWEBP_SUPPORT=OFF
|
||||
-DENABLE_WEBP=OFF
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE
|
||||
|
||||
2
.github/scripts/.package.zsh
vendored
2
.github/scripts/.package.zsh
vendored
@@ -259,7 +259,7 @@ ${_usage_host:-}"
|
||||
cmake --build build_${target##*-} --config ${config} -t package ${cmake_args}
|
||||
|
||||
# Mark certain deps as optional
|
||||
build-aux/CI/linux/demote-deps.sh ${project_root}/release/*.deb Recommends '(mqtt)|(opencv)|(tesseract)|(usb)|(x11)'
|
||||
build-aux/CI/linux/demote-deps.sh ${project_root}/release/*.deb Recommends '(mqtt)|(opencv)|(tesseract)|(usb)|(x11)|(libobs)'
|
||||
|
||||
if [ ! -e ${project_root}/release/${output_name}.deb ]; then
|
||||
mv ${project_root}/release/*.deb ${project_root}/release/${output_name}.deb
|
||||
|
||||
4
.github/scripts/Build-Deps-Windows.ps1
vendored
4
.github/scripts/Build-Deps-Windows.ps1
vendored
@@ -106,6 +106,7 @@ function Build {
|
||||
$ADVSSDepPath = "$(Resolve-Path -Path ${ProjectRoot}/${OutDirName})"
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVContribPath = "${ProjectRoot}/deps/opencv_contrib"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
|
||||
Push-Location -Stack BuildOpenCVTemp
|
||||
@@ -115,7 +116,8 @@ function Build {
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
|
||||
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
|
||||
"-DBUILD_LIST=core,imgproc,objdetect"
|
||||
"-DBUILD_LIST=core,imgproc,objdetect,xobjdetect"
|
||||
"-DOPENCV_EXTRA_MODULES_PATH:PATH=${OpenCVContribPath}/modules"
|
||||
)
|
||||
|
||||
Log-Information "Configuring OpenCV..."
|
||||
|
||||
32
.github/scripts/Package-Windows.ps1
vendored
32
.github/scripts/Package-Windows.ps1
vendored
@@ -96,25 +96,25 @@ function Package {
|
||||
Log-Group
|
||||
|
||||
# --- Legacy zip (old layout, extract to OBS install directory) ---
|
||||
Log-Group "Archiving ${ProductName} (legacy)..."
|
||||
$LegStaging = "${ProjectRoot}/release/zip-staging-leg"
|
||||
Remove-Item -Path $LegStaging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $LegStaging | Out-Null
|
||||
Copy-Item -Path "${CIWindowsDir}/README-legacy.txt" -Destination "${LegStaging}/README.txt"
|
||||
Log-Group "Archiving ${ProductName} (portable)..."
|
||||
$PortableStaging = "${ProjectRoot}/release/zip-staging-portable"
|
||||
Remove-Item -Path $PortableStaging -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force -Path $PortableStaging | Out-Null
|
||||
Copy-Item -Path "${CIWindowsDir}/README-portable.txt" -Destination "${PortableStaging}/README.txt"
|
||||
if ( Test-Path -Path $NewBinPath ) {
|
||||
$LegBinPath = "${LegStaging}/obs-plugins/64bit"
|
||||
New-Item -ItemType Directory -Force -Path $LegBinPath | Out-Null
|
||||
Copy-Item -Path "${NewBinPath}/*" -Destination $LegBinPath -Recurse -Force
|
||||
$PortableBinPath = "${PortableStaging}/obs-plugins/64bit"
|
||||
New-Item -ItemType Directory -Force -Path $PortableBinPath | Out-Null
|
||||
Copy-Item -Path "${NewBinPath}/*" -Destination $PortableBinPath -Recurse -Force
|
||||
}
|
||||
if ( Test-Path -Path $NewDataPath ) {
|
||||
$LegDataPath = "${LegStaging}/data/obs-plugins/${ProductName}"
|
||||
New-Item -ItemType Directory -Force -Path $LegDataPath | Out-Null
|
||||
Copy-Item -Path "${NewDataPath}/*" -Destination $LegDataPath -Recurse -Force
|
||||
$PortableDataPath = "${PortableStaging}/data/obs-plugins/${ProductName}"
|
||||
New-Item -ItemType Directory -Force -Path $PortableDataPath | Out-Null
|
||||
Copy-Item -Path "${NewDataPath}/*" -Destination $PortableDataPath -Recurse -Force
|
||||
}
|
||||
Compress-Archive -Force -Path (Get-ChildItem -Path $LegStaging) `
|
||||
Compress-Archive -Force -Path (Get-ChildItem -Path $PortableStaging) `
|
||||
-CompressionLevel Optimal `
|
||||
-DestinationPath "${ProjectRoot}/release/${OutputName}-legacy.zip"
|
||||
Remove-Item -Path $LegStaging -Recurse -Force
|
||||
-DestinationPath "${ProjectRoot}/release/${OutputName}-portable.zip"
|
||||
Remove-Item -Path $PortableStaging -Recurse -Force
|
||||
Log-Group
|
||||
|
||||
if ( ( $BuildInstaller ) ) {
|
||||
@@ -137,12 +137,12 @@ function Package {
|
||||
|
||||
# Legacy layout (for OBS installation directory)
|
||||
if ( Test-Path "${Configuration}/${ProductName}/bin/64bit" ) {
|
||||
$PkgLegBin = "Package/legacy/obs-plugins/64bit"
|
||||
$PkgLegBin = "Package/portable/obs-plugins/64bit"
|
||||
New-Item -ItemType Directory -Force -Path $PkgLegBin | Out-Null
|
||||
Copy-Item -Path "${Configuration}/${ProductName}/bin/64bit/*" -Destination $PkgLegBin -Recurse -Force
|
||||
}
|
||||
if ( Test-Path "${Configuration}/${ProductName}/data" ) {
|
||||
$PkgLegData = "Package/legacy/data/obs-plugins/${ProductName}"
|
||||
$PkgLegData = "Package/portable/data/obs-plugins/${ProductName}"
|
||||
New-Item -ItemType Directory -Force -Path $PkgLegData | Out-Null
|
||||
Copy-Item -Path "${Configuration}/${ProductName}/data/*" -Destination $PkgLegData -Recurse -Force
|
||||
}
|
||||
|
||||
7
.github/scripts/opencv-force-processor-x86_64.cmake
vendored
Normal file
7
.github/scripts/opencv-force-processor-x86_64.cmake
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Injected via CMAKE_PROJECT_INCLUDE after opencv's project() call. Forces
|
||||
# CMAKE_SYSTEM_PROCESSOR into the cache so that third-party subdirectories
|
||||
# (mlas) that read the cache directly see x86_64 instead of the arm64 host
|
||||
# processor on Apple Silicon CI runners.
|
||||
set(CMAKE_SYSTEM_PROCESSOR
|
||||
x86_64
|
||||
CACHE INTERNAL "" FORCE)
|
||||
21
.github/scripts/utils.zsh/setup_linux
vendored
21
.github/scripts/utils.zsh/setup_linux
vendored
@@ -35,14 +35,21 @@ if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
|
||||
sudo apt-get install ${apt_args} gcc-${${target##*-}//_/-}-linux-gnu g++-${${target##*-}//_/-}-linux-gnu
|
||||
}
|
||||
|
||||
sudo add-apt-repository --yes ppa:obsproject/obs-studio
|
||||
sudo apt update
|
||||
local dist_version
|
||||
read -r dist_version <<< "$(source /etc/os-release; print "${VERSION_ID}")"
|
||||
|
||||
sudo apt-get install ${apt_args} \
|
||||
build-essential \
|
||||
libgles2-mesa-dev \
|
||||
libsimde-dev \
|
||||
obs-studio
|
||||
# The OBS PPA may not yet support newer Ubuntu versions.
|
||||
# On those, use the native obs-studio packages and install libobs-dev separately,
|
||||
# as it is not bundled in obs-studio itself on newer Ubuntu releases.
|
||||
local -a obs_packages=(build-essential libgles2-mesa-dev libsimde-dev obs-studio)
|
||||
if is-at-least 26.04 ${dist_version}; then
|
||||
obs_packages+=(libobs-dev)
|
||||
else
|
||||
sudo add-apt-repository --yes ppa:obsproject/obs-studio
|
||||
sudo apt update
|
||||
fi
|
||||
|
||||
sudo apt-get install ${apt_args} ${obs_packages}
|
||||
|
||||
local -a _qt_packages=()
|
||||
|
||||
|
||||
106
.github/workflows/build-project.yaml
vendored
106
.github/workflows/build-project.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
description: "Project name detected by parsing build spec file"
|
||||
value: ${{ jobs.check-event.outputs.pluginName }}
|
||||
env:
|
||||
DEP_DIR: .deps/advss-build-dependencies-5
|
||||
DEP_DIR: .deps/advss-build-dependencies-6
|
||||
jobs:
|
||||
check-event:
|
||||
name: Check GitHub Event Data 🔎
|
||||
@@ -213,6 +213,17 @@ jobs:
|
||||
- name: Set up Homebrew 🍺
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Install Vulkan SDK 🌋
|
||||
shell: bash
|
||||
run: |
|
||||
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc \
|
||||
| sudo tee /etc/apt/trusted.gpg.d/lunarg.asc
|
||||
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-noble.list \
|
||||
https://packages.lunarg.com/vulkan/lunarg-vulkan-noble.list
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y vulkan-sdk
|
||||
echo "VULKAN_SDK=/usr" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
with:
|
||||
@@ -238,6 +249,14 @@ jobs:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-sources-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-source.*
|
||||
|
||||
- name: Rename artifacts for Ubuntu 24 🏷️
|
||||
run: |
|
||||
: Rename artifacts for Ubuntu 24 🏷️
|
||||
for f in ${{ github.workspace }}/release/*-x86_64-linux-gnu.*; do
|
||||
[ -e "${f}" ] || continue
|
||||
mv "${f}" "${f/x86_64-linux-gnu/x86_64-ubuntu24.04-linux-gnu}"
|
||||
done
|
||||
|
||||
- name: Upload Artifacts 📡
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -252,6 +271,85 @@ jobs:
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
|
||||
if-no-files-found: ignore
|
||||
|
||||
ubuntu-26-build:
|
||||
name: Build for Ubuntu 26 🐧
|
||||
runs-on: ubuntu-26.04
|
||||
needs: check-event
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Up Environment 🔧
|
||||
id: setup
|
||||
run: |
|
||||
: Set Up Environment 🔧
|
||||
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
|
||||
|
||||
git_tag="$(git describe --tags)"
|
||||
|
||||
read -r product_name product_version <<< \
|
||||
"$(jq -r '. | {name, version} | join(" ")' buildspec.json)"
|
||||
|
||||
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
|
||||
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v5
|
||||
id: ccache-cache
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ${{ runner.os }}-ccache-x86_64-${{ needs.check-event.outputs.config }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ccache-x86_64-
|
||||
|
||||
- name: Set up Homebrew 🍺
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
with:
|
||||
target: x86_64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
target: x86_64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Package Plugin 📀
|
||||
uses: ./.github/actions/package-plugin
|
||||
with:
|
||||
package: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||
target: x86_64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Rename artifacts for Ubuntu 26 🏷️
|
||||
run: |
|
||||
: Rename artifacts for Ubuntu 26 🏷️
|
||||
for f in ${{ github.workspace }}/release/*-x86_64-linux-gnu.*; do
|
||||
[ -e "${f}" ] || continue
|
||||
mv "${f}" "${f/x86_64-linux-gnu/x86_64-ubuntu26.04-linux-gnu}"
|
||||
done
|
||||
|
||||
- name: Upload Artifacts 📡
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-26.04-x86_64-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*.*
|
||||
|
||||
- name: Upload debug symbol artifacts 🪲
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-26.04-x86_64-${{ needs.check-event.outputs.commitHash }}-dbgsym
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
|
||||
if-no-files-found: ignore
|
||||
|
||||
windows-build:
|
||||
name: Build for Windows 🪟
|
||||
runs-on: windows-2022
|
||||
@@ -287,6 +385,12 @@ jobs:
|
||||
target: x64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Install Vulkan SDK 🌋
|
||||
uses: jakoch/install-vulkan-sdk-action@v1
|
||||
with:
|
||||
install_runtime: false
|
||||
cache: true
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
with:
|
||||
|
||||
1
.github/workflows/push.yaml
vendored
1
.github/workflows/push.yaml
vendored
@@ -78,6 +78,7 @@ jobs:
|
||||
'windows-x64;zip|exe'
|
||||
'macos-universal;tar.xz|pkg'
|
||||
'ubuntu-24.04-x86_64;tar.xz|deb|ddeb'
|
||||
'ubuntu-26.04-x86_64;tar.xz|deb|ddeb'
|
||||
'sources;tar.xz'
|
||||
)
|
||||
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -28,12 +28,15 @@
|
||||
[submodule "deps/libusb"]
|
||||
path = deps/libusb
|
||||
url = https://github.com/libusb/libusb.git
|
||||
[submodule "deps/date"]
|
||||
path = deps/date
|
||||
url = https://github.com/HowardHinnant/date.git
|
||||
[submodule "deps/jsoncons"]
|
||||
path = deps/jsoncons
|
||||
url = https://github.com/danielaparker/jsoncons.git
|
||||
[submodule "deps/paho.mqtt.cpp"]
|
||||
path = deps/paho.mqtt.cpp
|
||||
url = https://github.com/eclipse-paho/paho.mqtt.cpp.git
|
||||
[submodule "deps/opencv_contrib"]
|
||||
path = deps/opencv_contrib
|
||||
url = https://github.com/opencv/opencv_contrib.git
|
||||
[submodule "deps/whisper.cpp"]
|
||||
path = deps/whisper.cpp
|
||||
url = https://github.com/ggml-org/whisper.cpp.git
|
||||
|
||||
@@ -16,6 +16,14 @@ if(BUILD_OUT_OF_TREE)
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake"
|
||||
NO_POLICY_SCOPE)
|
||||
include(compilerconfig)
|
||||
# OPENSSL_USE_STATIC_LIBS here ensures FindOpenSSL picks up the static libs
|
||||
# preventing DLL name collisions with other plugins (e.g.NVIDIA AR SDK).
|
||||
if(OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
include(cmake/windows/wingetssl.cmake)
|
||||
endif()
|
||||
include(defaults)
|
||||
include(helpers)
|
||||
endif()
|
||||
@@ -34,11 +42,6 @@ include(cmake/common/get_git_revision_description.cmake)
|
||||
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
|
||||
git_describe(GIT_TAG)
|
||||
|
||||
# Helper for OpenSSL
|
||||
if(OS_WINDOWS)
|
||||
include(cmake/windows/wingetssl.cmake)
|
||||
endif()
|
||||
|
||||
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
|
||||
set(GIT_TAG ${PROJECT_VERSION})
|
||||
endif()
|
||||
@@ -104,6 +107,8 @@ target_sources(
|
||||
lib/macro/macro-action-edit.hpp
|
||||
lib/macro/macro-action-factory.cpp
|
||||
lib/macro/macro-action-factory.hpp
|
||||
lib/macro/macro-action-loop.cpp
|
||||
lib/macro/macro-action-loop.hpp
|
||||
lib/macro/macro-action-macro.cpp
|
||||
lib/macro/macro-action-macro.hpp
|
||||
lib/macro/macro-action-queue.cpp
|
||||
@@ -209,6 +214,13 @@ target_sources(
|
||||
lib/utils/filter-combo-box.hpp
|
||||
lib/utils/first-run-wizard.cpp
|
||||
lib/utils/first-run-wizard.hpp
|
||||
lib/utils/first-run-wizard-audio.cpp
|
||||
lib/utils/first-run-wizard-audio.hpp
|
||||
lib/utils/first-run-wizard-helpers.hpp
|
||||
lib/utils/first-run-wizard-sequence.cpp
|
||||
lib/utils/first-run-wizard-sequence.hpp
|
||||
lib/utils/first-run-wizard-window.cpp
|
||||
lib/utils/first-run-wizard-window.hpp
|
||||
lib/utils/help-icon.hpp
|
||||
lib/utils/help-icon.cpp
|
||||
lib/utils/item-selection-helpers.cpp
|
||||
@@ -291,6 +303,10 @@ target_sources(
|
||||
lib/utils/volume-control.hpp
|
||||
lib/utils/websocket-api.cpp
|
||||
lib/utils/websocket-api.hpp
|
||||
lib/variables/variable-color-button.cpp
|
||||
lib/variables/variable-color-button.hpp
|
||||
lib/variables/variable-color.cpp
|
||||
lib/variables/variable-color.hpp
|
||||
lib/variables/variable-line-edit.cpp
|
||||
lib/variables/variable-line-edit.hpp
|
||||
lib/variables/variable-number.hpp
|
||||
@@ -363,6 +379,9 @@ set_target_properties(${LIB_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
target_compile_features(${LIB_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
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")
|
||||
@@ -540,3 +559,59 @@ else()
|
||||
set_target_properties_obs(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- Install third-party dependency licenses ---
|
||||
#
|
||||
# Each entry is "display-name:path/to/license/file" relative to deps/. Files are
|
||||
# silently skipped when a submodule is not checked out.
|
||||
set(_dep_license_files
|
||||
"asio:asio/asio/LICENSE_1_0.txt"
|
||||
"cpp-httplib:cpp-httplib/LICENSE"
|
||||
"json:json/LICENSE.MIT"
|
||||
"jsoncons:jsoncons/LICENSE"
|
||||
"leptonica:leptonica/leptonica-license.txt"
|
||||
"libremidi:libremidi/LICENSE.md"
|
||||
"libusb:libusb/COPYING"
|
||||
"opencv:opencv/LICENSE"
|
||||
"openvr:openvr/LICENSE"
|
||||
"paho-mqtt:paho.mqtt.cpp/LICENSE"
|
||||
"tesseract:tesseract/LICENSE"
|
||||
"websocketpp:websocketpp/COPYING"
|
||||
"whisper:whisper.cpp/LICENSE")
|
||||
|
||||
if(DEB_INSTALL)
|
||||
if(NOT DATA_OUT_DIR)
|
||||
set(DATA_OUT_DIR "/share/obs/obs-plugins/${PROJECT_NAME}")
|
||||
endif()
|
||||
set(_license_dest "${CMAKE_INSTALL_PREFIX}/${DATA_OUT_DIR}/licenses")
|
||||
elseif(OS_MACOS)
|
||||
# macOS: license files become bundle Resources/licenses/* entries
|
||||
set(_license_dest "")
|
||||
else()
|
||||
# Windows and non-DEB Linux: alongside the plugin's data directory
|
||||
set(_license_dest "${CMAKE_PROJECT_NAME}/data/licenses")
|
||||
endif()
|
||||
|
||||
foreach(_entry IN LISTS _dep_license_files)
|
||||
string(REGEX MATCH "^([^:]+):(.+)$" _m "${_entry}")
|
||||
set(_dep_name "${CMAKE_MATCH_1}")
|
||||
set(_dep_file "${CMAKE_CURRENT_SOURCE_DIR}/deps/${CMAKE_MATCH_2}")
|
||||
|
||||
if(NOT EXISTS "${_dep_file}")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
if(OS_MACOS)
|
||||
# Copy to binary dir so we can give the file a clean name
|
||||
set(_staged "${CMAKE_CURRENT_BINARY_DIR}/licenses/LICENSE-${_dep_name}")
|
||||
configure_file("${_dep_file}" "${_staged}" COPYONLY)
|
||||
target_sources(${PROJECT_NAME} PRIVATE "${_staged}")
|
||||
set_source_files_properties("${_staged}" PROPERTIES MACOSX_PACKAGE_LOCATION
|
||||
"Resources/licenses")
|
||||
else()
|
||||
install(
|
||||
FILES "${_dep_file}"
|
||||
DESTINATION "${_license_dest}"
|
||||
RENAME "LICENSE-${_dep_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
Advanced Scene Switcher - Windows Installation (Legacy)
|
||||
========================================================
|
||||
Advanced Scene Switcher - Windows Installation (Portable)
|
||||
==========================================================
|
||||
|
||||
For full installation instructions visit:
|
||||
https://github.com/WarmUpTill/SceneSwitcher/wiki/Installation
|
||||
|
||||
|
||||
This archive uses the legacy plugin layout for older OBS versions,
|
||||
This archive uses the portable plugin layout for older OBS versions,
|
||||
portable OBS installs, and Steam.
|
||||
|
||||
Extract the CONTENTS of this archive into your OBS installation directory,
|
||||
@@ -17,5 +17,5 @@ After extracting, the path should look like:
|
||||
C:\ProgramData\obs-studio\plugins\advanced-scene-switcher\bin\64bit\advanced-scene-switcher.dll
|
||||
|
||||
|
||||
If the plugin does not appear in OBS, try the legacy archive instead:
|
||||
advanced-scene-switcher-<version>-windows-x64-legacy.zip
|
||||
If the plugin does not appear in OBS, try the portable archive instead:
|
||||
advanced-scene-switcher-<version>-windows-x64-portable.zip
|
||||
|
||||
@@ -333,6 +333,8 @@ function(setup_advss_plugin target)
|
||||
PRIVATE "${ADVSS_SOURCE_DIR}/lib" "${ADVSS_SOURCE_DIR}/lib/legacy"
|
||||
"${ADVSS_SOURCE_DIR}/lib/macro" "${ADVSS_SOURCE_DIR}/lib/utils"
|
||||
"${ADVSS_SOURCE_DIR}/lib/variables" "${ADVSS_SOURCE_DIR}/forms")
|
||||
|
||||
target_precompile_headers(${target} PRIVATE "${ADVSS_SOURCE_DIR}/lib/pch.hpp")
|
||||
endfunction()
|
||||
|
||||
function(install_advss_plugin_dependency)
|
||||
|
||||
@@ -22,6 +22,7 @@ set(CPACK_GENERATOR
|
||||
"DEB"
|
||||
CACHE STRING "CPack generator to use")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "obs-studio")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
|
||||
set(CPACK_SET_DESTDIR ON)
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
; Recommended layout - used when installing to %ProgramData%\obs-studio\plugins\
|
||||
Source: "..\release\Package\recommended\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsRecommendedLayout
|
||||
; Legacy layout - used when installing to the OBS installation directory
|
||||
Source: "..\release\Package\legacy\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsLegacyLayout
|
||||
Source: "..\release\Package\portable\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Check: IsLegacyLayout
|
||||
Source: "..\LICENSE"; Flags: dontcopy
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
@@ -88,9 +88,9 @@ begin
|
||||
Result := PrevPath;
|
||||
end;
|
||||
|
||||
// Remove files placed by the legacy installer (obs-plugins/64bit and
|
||||
// Remove files placed by the portable installer (obs-plugins/64bit and
|
||||
// data/obs-plugins layout) so the plugin is not loaded twice by OBS.
|
||||
// Only runs when upgrading from a legacy location to the recommended one.
|
||||
// Only runs when upgrading from a portable location to the recommended one.
|
||||
procedure RemoveLegacyFiles();
|
||||
var
|
||||
OldInstallPath: string;
|
||||
|
||||
@@ -40,19 +40,27 @@ if(WIN32 AND (NOT OpenSSL_FOUND))
|
||||
message(STATUS "Looking for OpenSSL built with CRT variant: ${_crt_kind}")
|
||||
|
||||
# Try to find the root and corresponding lib path
|
||||
if(OPENSSL_USE_STATIC_LIBS)
|
||||
set(_crypto_lib_name "libcrypto_static.lib")
|
||||
set(_ssl_lib_name "libssl_static.lib")
|
||||
else()
|
||||
set(_crypto_lib_name "libcrypto.lib")
|
||||
set(_ssl_lib_name "libssl.lib")
|
||||
endif()
|
||||
|
||||
foreach(_root ${_openssl_roots})
|
||||
if(EXISTS "${_root}/include/openssl/ssl.h")
|
||||
foreach(_suffix ${_openssl_lib_suffixes})
|
||||
if(_suffix MATCHES "${_crt_kind}$"
|
||||
AND EXISTS "${_root}/${_suffix}/libcrypto.lib")
|
||||
AND EXISTS "${_root}/${_suffix}/${_crypto_lib_name}")
|
||||
set(OPENSSL_ROOT_DIR
|
||||
"${_root}"
|
||||
CACHE PATH "Path to OpenSSL root")
|
||||
set(OPENSSL_CRYPTO_LIBRARY
|
||||
"${_root}/${_suffix}/libcrypto.lib"
|
||||
"${_root}/${_suffix}/${_crypto_lib_name}"
|
||||
CACHE FILEPATH "OpenSSL crypto lib")
|
||||
set(OPENSSL_SSL_LIBRARY
|
||||
"${_root}/${_suffix}/libssl.lib"
|
||||
"${_root}/${_suffix}/${_ssl_lib_name}"
|
||||
CACHE FILEPATH "OpenSSL ssl lib")
|
||||
set(OPENSSL_INCLUDE_DIR
|
||||
"${_root}/include"
|
||||
|
||||
@@ -382,8 +382,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Warten, bis der Übergan
|
||||
AdvSceneSwitcher.action.wait="Warten"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="fixe"
|
||||
AdvSceneSwitcher.action.wait.type.random="zufällig"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Warte für {{waitType}} Dauer von {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Warte für {{waitType}} Dauer von {{duration}} bis {{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Warte für {{waitType}} Dauer von {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Warte für {{waitType}} Dauer von {{duration}} bis {{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Audio"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Stummgeschaltet"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Nicht stummgeschaltet"
|
||||
|
||||
@@ -206,6 +206,8 @@ AdvSceneSwitcher.macroTab.expandAllGroups="Expand all Groups"
|
||||
AdvSceneSwitcher.macroTab.collapseAllGroups="Collapse all Groups"
|
||||
AdvSceneSwitcher.macroTab.rename="Rename"
|
||||
AdvSceneSwitcher.macroTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroTab.pause="Pause"
|
||||
AdvSceneSwitcher.macroTab.unpause="Unpause"
|
||||
AdvSceneSwitcher.macroTab.export="Export"
|
||||
AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:"
|
||||
AdvSceneSwitcher.macroTab.export.usePlainText="Use plain text"
|
||||
@@ -444,6 +446,7 @@ AdvSceneSwitcher.condition.video.ocrConfigReload="Reload configuration file"
|
||||
AdvSceneSwitcher.condition.video.ocrConfigHint="Tesseract config files consist of lines with parameter-value pairs (space separated).\nFor example:\n\ntessedit_char_blacklist\t\t\t\t\"abc\"\nlanguage_model_penalty_non_dict_word\t0"
|
||||
AdvSceneSwitcher.condition.video.modelLoadFail="Model data could not be loaded!"
|
||||
AdvSceneSwitcher.condition.video.selectColor="Select Color"
|
||||
AdvSceneSwitcher.condition.video.colorVariableTooltip="Expected format: #RRGGBB or #AARRGGBB\nInvalid values will default to black."
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleColumn="Single column of text of variable sizes"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText="Single uniform block of vertically aligned text"
|
||||
AdvSceneSwitcher.condition.video.ocrMode.singleBlock="Single uniform block of text"
|
||||
@@ -467,12 +470,12 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="Minimum neighbors:{{minNeig
|
||||
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}Reduce CPU load by performing check only every{{throttleCount}}milliseconds"
|
||||
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="Perform check only in area"
|
||||
AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Check for text color:{{textColor}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Check for text color:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrTextType="Check for text type:{{textType}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrBaseDir="Tesseract base directory:{{tesseractBaseDir}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrLanguage="Check for language:{{languageCode}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrConfig="Config file:{{configFile}}{{openConfigFile}}{{reloadConfig}}{{configFileHint}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="Check for color:{{color}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="Check for color:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="Minimum size:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="Maximum size:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="Select area"
|
||||
@@ -837,6 +840,38 @@ AdvSceneSwitcher.condition.clipboard.condition.isImage="Clipboard contains an im
|
||||
AdvSceneSwitcher.condition.clipboard.condition.isURL="Clipboard contains an URL"
|
||||
AdvSceneSwitcher.condition.clipboard.condition.matches="Clipboard content matches"
|
||||
AdvSceneSwitcher.condition.clipboard.condition.entry="{{conditions}}{{regex}}{{urlInfo}}"
|
||||
AdvSceneSwitcher.condition.speech="Speech Recognition (beta)"
|
||||
AdvSceneSwitcher.condition.speech.condition.any="Any speech is detected for"
|
||||
AdvSceneSwitcher.condition.speech.condition.contains="contains phrase"
|
||||
AdvSceneSwitcher.condition.speech.condition.matches="matches"
|
||||
AdvSceneSwitcher.condition.speech.layout.any="{{conditions}}{{source}}"
|
||||
AdvSceneSwitcher.condition.speech.layout.contains="Transcript of{{source}}{{conditions}}:"
|
||||
AdvSceneSwitcher.condition.speech.layout.matches="Transcript of{{source}}{{conditions}}:"
|
||||
AdvSceneSwitcher.condition.speech.layout.phrase="{{phrase}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.speech.layout.model="Whisper model:{{modelPath}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.model.help="GGML model files can be downloaded from https://huggingface.co/ggerganov/whisper.cpp (e.g. ggml-base.bin).\nLarger models are more accurate but slower."
|
||||
AdvSceneSwitcher.condition.speech.layout.buffer="Audio buffer:{{bufferDuration}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.browse="Browse..."
|
||||
AdvSceneSwitcher.condition.speech.browse.title="Select Whisper model file"
|
||||
AdvSceneSwitcher.condition.speech.browse.filter="GGML model files (*.bin);;All files (*)"
|
||||
AdvSceneSwitcher.condition.speech.buffer.help="Longer buffer durations improve accuracy but increase latency."
|
||||
AdvSceneSwitcher.condition.speech.advanced="Advanced"
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.threads="Threads:{{threads}}"
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.language="Language:{{language}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.language.help="Whisper language code (e.g. 'en', 'de', 'fr') or 'auto' to detect automatically."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.translate="{{translate}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.translate="Translate to English"
|
||||
AdvSceneSwitcher.condition.speech.advanced.translate.help="Translate non-English speech to English before transcribing.\nUseful when the phrase or regex is written in English but the source speaks another language."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.vad="VAD energy threshold:{{vad}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.vad.help="Minimum RMS energy a buffer must have before running inference. Buffers below this level are treated as silence and skipped. Lower values are more sensitive; raise it if inference triggers on background noise."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.suppress="{{suppress}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.suppress="Suppress non-speech tokens"
|
||||
AdvSceneSwitcher.condition.speech.advanced.suppress.help="Remove filler tokens such as [MUSIC] or (applause) that Whisper tends to insert when it detects non-speech sounds."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.noContext="{{noContext}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.noContext="No context"
|
||||
AdvSceneSwitcher.condition.speech.advanced.noContext.help="Do not feed the previous transcription back as a prompt for the next buffer.\nPrevents repetition across buffer boundaries at the cost of slightly reduced coherence."
|
||||
AdvSceneSwitcher.condition.speech.advanced.listenWhenMuted="Listen when source is muted"
|
||||
AdvSceneSwitcher.condition.speech.advanced.useGpu="Use GPU"
|
||||
AdvSceneSwitcher.condition.folder="Folder watch"
|
||||
AdvSceneSwitcher.condition.folder.tooltip="This condition type will allow you to monitor the contents of a folder.\nNote that the monitoring will *not* recursively scan for changes in sub directories within directories of the selected folder!\nNote that if there are several changes during a short period of time, some of the changes might not emit this signal.\nHowever, the last change in the sequence of changes always will."
|
||||
AdvSceneSwitcher.condition.folder.condition.any="Any change happened"
|
||||
@@ -898,8 +933,18 @@ AdvSceneSwitcher.action.scene.canvasNotSupported="Not supported by the currently
|
||||
AdvSceneSwitcher.action.wait="Wait"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="fixed"
|
||||
AdvSceneSwitcher.action.wait.type.random="random"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Wait for{{waitType}}duration of{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Wait for{{waitType}}duration from{{duration}}to{{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.type.variableCondition="variable"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Wait for{{waitType}}duration of{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Wait for{{waitType}}duration from{{duration}}to{{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.condition.equals="equals"
|
||||
AdvSceneSwitcher.action.wait.condition.doesNotEqual="does not equal"
|
||||
AdvSceneSwitcher.action.wait.condition.isEmpty="is empty"
|
||||
AdvSceneSwitcher.action.wait.condition.lessThan="is less than"
|
||||
AdvSceneSwitcher.action.wait.condition.greaterThan="is greater than"
|
||||
AdvSceneSwitcher.action.wait.layout.variableCondition="Wait until{{waitType}}{{variable}}{{condition}}{{strValue}}{{numValue}}{{regex}}"
|
||||
AdvSceneSwitcher.action.wait.layout.timeout="Timeout:{{useTimeout}}{{conditionTimeout}}"
|
||||
AdvSceneSwitcher.tempVar.wait.timedOut="Timed out"
|
||||
AdvSceneSwitcher.tempVar.wait.timedOut.description="True if the wait action timed out before the condition was met, false if the condition was met in time"
|
||||
AdvSceneSwitcher.action.audio="Audio"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Mute"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Unmute"
|
||||
@@ -939,7 +984,8 @@ AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="Change filename f
|
||||
AdvSceneSwitcher.action.recording.type.addChapter="Add chapter"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="Note that depending on your recording settings you might not be able to pause recording"
|
||||
AdvSceneSwitcher.action.recording.split.hint="Make sure to enable automatic file splitting in the OBS settings first!"
|
||||
AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{chapterName}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.recording.outputNotActive.hint="This change will only take effect when the Recording, Replay Buffer, ... output is not active.\nThe output needs to be restarted for the change to apply."
|
||||
AdvSceneSwitcher.action.recording.layout="{{actions}}{{recordFolder}}{{recordFileFormat}}{{outputNotActiveHelp}}{{chapterName}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="Replay buffer"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="Warning: Saving too frequently might result in the replay buffer not actually being saved!"
|
||||
AdvSceneSwitcher.action.replay.durationWarn="Warning: Changing the maximum replay time will only apply the next time the replay buffer is started!"
|
||||
@@ -970,6 +1016,7 @@ AdvSceneSwitcher.action.sceneVisibility.type.sourceGroup="Any"
|
||||
AdvSceneSwitcher.action.sceneVisibility.layout="On{{scenes}}{{actions}}{{sources}}"
|
||||
AdvSceneSwitcher.action.sceneVisibility.layout.transition="{{updateTransition}}Set transition to{{transitions}}"
|
||||
AdvSceneSwitcher.action.sceneVisibility.layout.duration="{{updateDuration}}Set transition duration to{{duration}}seconds"
|
||||
AdvSceneSwitcher.action.sceneVisibility.blockUntilTransitionDone="Wait until visibility transition is complete"
|
||||
AdvSceneSwitcher.action.filter="Filter"
|
||||
AdvSceneSwitcher.action.filter.type.enable="Enable"
|
||||
AdvSceneSwitcher.action.filter.type.disable="Disable"
|
||||
@@ -981,7 +1028,7 @@ AdvSceneSwitcher.action.filter.refresh.tooltip="Repopulate the filter settings s
|
||||
AdvSceneSwitcher.action.filter.entry="On{{sources}}{{actions}}{{filters}}{{refresh}}{{settingsButtons}}"
|
||||
AdvSceneSwitcher.action.filter.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
|
||||
AdvSceneSwitcher.action.filter.getSettings="Get current settings"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to fixed value"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to value"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualListEntryManual="Set to list entry"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualTempvar="Set to macro property"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.json="Set setting JSON string"
|
||||
@@ -1017,7 +1064,7 @@ AdvSceneSwitcher.action.source.deinterlaceMode.yadif="Yadif"
|
||||
AdvSceneSwitcher.action.source.deinterlaceMode.yadif2x="Yadif 2x"
|
||||
AdvSceneSwitcher.action.source.deinterlaceOrder.topFieldFirst="Top Field First"
|
||||
AdvSceneSwitcher.action.source.deinterlaceOrder.bottomFieldFirst="Bottom Field First"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to fixed value"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to value"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualListEntryManual="Set to list entry"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualTempvar="Set to macro property"
|
||||
AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
|
||||
@@ -1109,6 +1156,12 @@ AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.id="of action type"
|
||||
AdvSceneSwitcher.action.loop="Loop"
|
||||
AdvSceneSwitcher.action.loop.maxIterations="Max iterations"
|
||||
AdvSceneSwitcher.action.loop.conditionHelp="Define the loop condition. The actions below will be repeated while these conditions are met.\n\nClick the plus button below to add a new condition."
|
||||
AdvSceneSwitcher.action.loop.actionHelp="Define the actions to perform each iteration.\n\nClick the plus button below to add a new action."
|
||||
AdvSceneSwitcher.tempVar.loop.count="Iteration count"
|
||||
AdvSceneSwitcher.tempVar.loop.count.description="The total number of iterations completed."
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro.conditionHelp="This section allows you to define macro conditions.\n\nClick the plus button below to add a new condition."
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro.actionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are met.\n\nClick the plus button below to add a new action."
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro.elseActionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are *not* met.\n\nClick the plus button below to add a new action."
|
||||
@@ -1288,6 +1341,8 @@ AdvSceneSwitcher.action.variable.type.stringLength="Set to length of string"
|
||||
AdvSceneSwitcher.action.variable.type.extractJsonField="Extract JSON field with name"
|
||||
AdvSceneSwitcher.action.variable.type.queryJson="Query JSON"
|
||||
AdvSceneSwitcher.action.variable.type.queryJson.info="You can use \"JSONPath\" (RFC 9535) syntax here.\nSo, for example:\n\n • $['books'][0]['category']\n • $.books[0].category\n\nIf the query or the JSON should be invalid, the variable value will not be changed.\nIf the input is valid, the result of the query will always be a JSON array."
|
||||
AdvSceneSwitcher.action.variable.type.queryJson.extractSingle="Extract value if single result"
|
||||
AdvSceneSwitcher.action.variable.type.queryJson.extractSingle.info="When enabled and the query returns exactly one result, the value is extracted from the result array and stored directly.\nIf the query matches zero or more than one element, the full JSON array is stored as usual."
|
||||
AdvSceneSwitcher.action.variable.type.accessJsonArray="Access JSON array at index"
|
||||
AdvSceneSwitcher.action.variable.type.setToTempvar="Set to macro property"
|
||||
AdvSceneSwitcher.action.variable.type.setToTempvar.help="This action type will allow you to extract values out of segments of the current macro and assign those values to the selected variable.\nFor example, you can get the current scene name from a scene condition and assign this name to a variable."
|
||||
@@ -1320,6 +1375,7 @@ AdvSceneSwitcher.action.variable.actionNoVariableSupport="Getting variable value
|
||||
AdvSceneSwitcher.action.variable.conditionNoVariableSupport="Getting variable values from %1 conditions is not supported!"
|
||||
AdvSceneSwitcher.action.variable.currentSegmentValue="Current value:"
|
||||
AdvSceneSwitcher.action.variable.layout.other="{{actions}}{{variables}}{{variables2}}{{strValue}}{{numValue}}{{segmentIndex}}{{mathExpression}}{{envVariableName}}{{scenes}}{{tempVars}}{{tempVarsHelp}}{{sceneItemIndex}}{{direction}}{{stringLength}}{{paddingCharSelection}}{{caseType}}{{jsonQuery}}{{jsonQueryHelp}}{{jsonIndex}}"
|
||||
AdvSceneSwitcher.action.variable.layout.copy="{{actions}}{{variables2}}to{{variables}}"
|
||||
AdvSceneSwitcher.action.variable.layout.pad="{{actions}}of{{variables}}to length{{stringLength}}by adding{{paddingCharSelection}}to the{{direction}}"
|
||||
AdvSceneSwitcher.action.variable.layout.truncate="{{actions}}of{{variables}}to length{{stringLength}}by removing characters from the{{direction}}"
|
||||
AdvSceneSwitcher.action.variable.layout.substringIndex="Substring start:{{subStringStart}}Substring size:{{subStringSize}}{{subStringRegex}}"
|
||||
@@ -1359,6 +1415,8 @@ AdvSceneSwitcher.action.twitch.type.channel.info.language.set="Set stream langua
|
||||
AdvSceneSwitcher.action.twitch.type.raid.start="Start raid"
|
||||
AdvSceneSwitcher.action.twitch.type.raid.end="Cancel raid"
|
||||
AdvSceneSwitcher.action.twitch.type.shoutout.send="Send shoutout"
|
||||
AdvSceneSwitcher.action.twitch.type.poll.start="Start poll"
|
||||
AdvSceneSwitcher.action.twitch.type.poll.end="End poll"
|
||||
AdvSceneSwitcher.action.twitch.type.shieldMode.start="Enable shield mode"
|
||||
AdvSceneSwitcher.action.twitch.type.shieldMode.end="Disable shield mode"
|
||||
AdvSceneSwitcher.action.twitch.type.commercial.start="Start commercial"
|
||||
@@ -1390,6 +1448,7 @@ AdvSceneSwitcher.action.twitch.type.user.vip.add="Add user as VIP"
|
||||
AdvSceneSwitcher.action.twitch.type.user.vip.delete="Remove user as VIP"
|
||||
AdvSceneSwitcher.action.twitch.type.reward.getInfo="Get channel points reward information"
|
||||
AdvSceneSwitcher.action.twitch.type.channel.getInfo="Get channel information"
|
||||
AdvSceneSwitcher.action.twitch.type.channel.ads.getInfo="Get channel ad schedule"
|
||||
AdvSceneSwitcher.action.twitch.reward.toggleControl="Toggle reward name / variable selection control"
|
||||
AdvSceneSwitcher.action.twitch.categorySelectionDisabled="Cannot select category without selecting a Twitch account first!"
|
||||
AdvSceneSwitcher.action.twitch.layout.default="On{{account}}{{actions}}{{streamTitle}}{{category}}{{markerDescription}}{{clipHasDelay}}{{duration}}{{announcementColor}}{{nonModDelayDuration}}{{channel}}{{pointsReward}}"
|
||||
@@ -1403,6 +1462,10 @@ AdvSceneSwitcher.action.twitch.layout.user.ban.row1="Using account{{account}}{{a
|
||||
AdvSceneSwitcher.action.twitch.layout.user.ban.row2="on channel{{channel}}for{{userInfoQueryType}}{{userLogin}}{{userId}}reason{{banReason}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row1="Using account{{account}}{{actions}}for channel{{channel}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.reward.getInfo.row2="{{pointsReward}}{{rewardVariable}}{{toggleRewardSelection}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.poll.start.row1="Using account{{account}}{{actions}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.poll.start.row2="on{{channel}}for{{duration}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.poll.end.row1="Using account{{account}}{{actions}}"
|
||||
AdvSceneSwitcher.action.twitch.layout.poll.end.row2="on{{channel}}{{pollEndStatus}}"
|
||||
AdvSceneSwitcher.action.twitch.title.title="Enter title"
|
||||
AdvSceneSwitcher.action.twitch.marker.description="Describe marker"
|
||||
AdvSceneSwitcher.action.twitch.clip.hasDelay="Add a slight delay before capturing the clip"
|
||||
@@ -1412,6 +1475,9 @@ AdvSceneSwitcher.action.twitch.announcement.blue="Blue"
|
||||
AdvSceneSwitcher.action.twitch.announcement.green="Green"
|
||||
AdvSceneSwitcher.action.twitch.announcement.orange="Orange"
|
||||
AdvSceneSwitcher.action.twitch.announcement.purple="Purple"
|
||||
AdvSceneSwitcher.action.twitch.poll.choices="Enter one choice per line (2-10 choices, max 25 characters each)"
|
||||
AdvSceneSwitcher.action.twitch.poll.end.terminated="Terminate (show results)"
|
||||
AdvSceneSwitcher.action.twitch.poll.end.archived="Archive (hide results)"
|
||||
AdvSceneSwitcher.action.twitch.user.getInfo.queryType.id="User Id"
|
||||
AdvSceneSwitcher.action.twitch.user.getInfo.queryType.login="User login"
|
||||
AdvSceneSwitcher.action.twitch.tags.add="Add Channel Tag"
|
||||
@@ -1596,6 +1662,8 @@ AdvSceneSwitcher.actionQueues.invalid="Invalid action queue selection"
|
||||
AdvSceneSwitcher.actionQueues.name="Name:"
|
||||
AdvSceneSwitcher.actionQueues.runOnStartup="Run action queue when starting the plugin"
|
||||
AdvSceneSwitcher.actionQueues.resolveVariablesOnAdd="Resolve variables when action is inserted into the queue"
|
||||
AdvSceneSwitcher.actionQueues.cloneVariableContext="Clone variable context when action is inserted into the queue"
|
||||
AdvSceneSwitcher.actionQueues.cloneVariableContext.tooltip="Captures the current values of all variables when the action is added to the queue.\nDuring execution, actions read from and write to this snapshot instead of the global variables.\nUnlike \"Resolve variables\", changes made by one action in the queue are visible to subsequent actions."
|
||||
AdvSceneSwitcher.actionQueues.running="Queue is running"
|
||||
AdvSceneSwitcher.actionQueues.stopped="Queue is stopped"
|
||||
AdvSceneSwitcher.actionQueues.start="Start action queue"
|
||||
@@ -1883,8 +1951,10 @@ AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.message="Subscriber Twitch
|
||||
AdvSceneSwitcher.tempVar.twitch.user_login.subscribe.message.description="The user login of the user who sent a resubscription chat message."
|
||||
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.message="Subscriber Twitch user name"
|
||||
AdvSceneSwitcher.tempVar.twitch.user_name.subscribe.message.description="The user display name of the user who a resubscription chat message."
|
||||
AdvSceneSwitcher.tempVar.twitch.message.subscribe="Subscription message"
|
||||
AdvSceneSwitcher.tempVar.twitch.message.subscribe.description="An object that contains the resubscription message and emote information needed to recreate the message."
|
||||
AdvSceneSwitcher.tempVar.twitch.message.subscribe="Subscription message (JSON)"
|
||||
AdvSceneSwitcher.tempVar.twitch.message.subscribe.description="A JSON object containing the resubscription message text and emote information. Use 'Subscription message text' to get the plain text."
|
||||
AdvSceneSwitcher.tempVar.twitch.message_text.subscribe="Subscription message text"
|
||||
AdvSceneSwitcher.tempVar.twitch.message_text.subscribe.description="The plain text of the resubscription message, with emotes replaced by their names."
|
||||
AdvSceneSwitcher.tempVar.twitch.cumulative_months.subscribe="Number of months subscribed"
|
||||
AdvSceneSwitcher.tempVar.twitch.cumulative_months.subscribe.description="The total number of months the user has been subscribed to the channel."
|
||||
AdvSceneSwitcher.tempVar.twitch.streak_months.subscribe="Subscription streak"
|
||||
@@ -2273,6 +2343,19 @@ AdvSceneSwitcher.tempVar.twitch.requester_user_login.commercial.description="The
|
||||
AdvSceneSwitcher.tempVar.twitch.requester_user_name.commercial="Requester user name"
|
||||
AdvSceneSwitcher.tempVar.twitch.requester_user_name.commercial.description="The display name of the user that requested the ad."
|
||||
|
||||
AdvSceneSwitcher.tempVar.twitch.snooze_count.ads="Snooze count"
|
||||
AdvSceneSwitcher.tempVar.twitch.snooze_count.ads.description="The number of snoozes available for the broadcaster."
|
||||
AdvSceneSwitcher.tempVar.twitch.snooze_refresh_at.ads="Snooze refresh time"
|
||||
AdvSceneSwitcher.tempVar.twitch.snooze_refresh_at.ads.description="The UTC timestamp of when the broadcaster will have an additional snooze available, in RFC3339 format."
|
||||
AdvSceneSwitcher.tempVar.twitch.next_ad_at.ads="Next ad time"
|
||||
AdvSceneSwitcher.tempVar.twitch.next_ad_at.ads.description="The UTC timestamp of when the channel's next scheduled ad break will begin, in RFC3339 format."
|
||||
AdvSceneSwitcher.tempVar.twitch.duration.ads="Ad duration"
|
||||
AdvSceneSwitcher.tempVar.twitch.duration.ads.description="The length in seconds of the scheduled ad break."
|
||||
AdvSceneSwitcher.tempVar.twitch.last_ad_at.ads="Last ad time"
|
||||
AdvSceneSwitcher.tempVar.twitch.last_ad_at.ads.description="The UTC timestamp of the channel's last ad break, in RFC3339 format."
|
||||
AdvSceneSwitcher.tempVar.twitch.preroll_free_time.ads="Pre-roll free time"
|
||||
AdvSceneSwitcher.tempVar.twitch.preroll_free_time.ads.description="The amount of time in seconds the broadcaster must wait before running another commercial."
|
||||
|
||||
AdvSceneSwitcher.tempVar.audio.output_volume="Output volume"
|
||||
AdvSceneSwitcher.tempVar.audio.output_volume.description="The volume the audio source is outputting."
|
||||
AdvSceneSwitcher.tempVar.audio.configured_volume="Configured volume"
|
||||
@@ -2409,6 +2492,9 @@ AdvSceneSwitcher.tempVar.streaming.serviceName.description="The name of the stre
|
||||
AdvSceneSwitcher.tempVar.clipboard.text="Clipboard text"
|
||||
AdvSceneSwitcher.tempVar.clipboard.text.description="The text contained in the clipboard.\nWill be empty if the clipboard does not contain text."
|
||||
|
||||
AdvSceneSwitcher.tempVar.speech.speech="Transcribed speech"
|
||||
AdvSceneSwitcher.tempVar.speech.speech.description="The text transcribed from the last audio buffer. Only populated when the condition matched."
|
||||
|
||||
AdvSceneSwitcher.tempVar.file.content="File content"
|
||||
AdvSceneSwitcher.tempVar.file.date="File modification date"
|
||||
AdvSceneSwitcher.tempVar.file.basename="File basename"
|
||||
@@ -2675,7 +2761,28 @@ FirstRunWizard.windowTitle="Advanced Scene Switcher - Setup Wizard"
|
||||
|
||||
FirstRunWizard.welcome.title="Welcome to Advanced Scene Switcher"
|
||||
FirstRunWizard.welcome.subtitle="Let's create your first automation in a few easy steps."
|
||||
FirstRunWizard.welcome.body="<p>Advanced Scene Switcher lets you build <b>Macros</b> - rules of the form:</p><blockquote><i>When [condition] -> perform [action]</i></blockquote><p>This wizard creates a macro that <b>switches to a chosen OBS scene whenever a specific window comes into focus</b>.</p><p>You can skip at any time. Re-open this wizard later from the <b>General</b> tab inside the Advanced Scene Switcher dialog.</p>"
|
||||
FirstRunWizard.welcome.body="<p>Advanced Scene Switcher lets you build <b>Macros</b> - rules of the form:</p><blockquote><i>When [condition] -> perform [action]</i></blockquote><p>This wizard will guide you through creating your first automation. On the next page, choose the type of macro you want to set up.</p><p>You can skip at any time. Re-open this wizard later from the <b>General</b> tab inside the Advanced Scene Switcher dialog.</p>"
|
||||
|
||||
FirstRunWizard.template.title="Choose an Automation Type"
|
||||
FirstRunWizard.template.subtitle="Select the kind of macro you would like to create."
|
||||
FirstRunWizard.template.window="Switch to a scene when a window comes into focus"
|
||||
FirstRunWizard.template.sequence="Run a timed sequence of scene switches"
|
||||
FirstRunWizard.template.audio="Show or hide a source based on audio activity"
|
||||
|
||||
FirstRunWizard.audio.source.title="Choose an Audio Source"
|
||||
FirstRunWizard.audio.source.subtitle="Select the audio source to monitor and configure when it should be considered active."
|
||||
FirstRunWizard.audio.source.sourceRow="Audio source:{{source}}"
|
||||
FirstRunWizard.audio.source.thresholdRow="Show when volume is above{{threshold}}for{{duration}}"
|
||||
|
||||
FirstRunWizard.audio.target.title="Choose a Target Source"
|
||||
FirstRunWizard.audio.target.subtitle="Select the source to show when audio is active and hide when it is not."
|
||||
FirstRunWizard.audio.target.sourceRow="Source to show/hide:{{source}}"
|
||||
|
||||
FirstRunWizard.audio.review.title="Review Your Macro"
|
||||
FirstRunWizard.audio.review.subtitle="Click Back to make changes, or Finish to create the macro."
|
||||
FirstRunWizard.audio.review.summary="<b>When</b> <i>%1</i> output volume is above <b>%2 dB</b> for <b>%3</b>:<br><br> <b>Show</b> <i>%4</i> on the current scene<br><br><b>Otherwise:</b><br><br> <b>Hide</b> <i>%4</i> on the current scene"
|
||||
FirstRunWizard.audio.review.errorTitle="Macro Creation Failed"
|
||||
FirstRunWizard.audio.review.errorBody="The audio macro could not be created. Please check the OBS log for details."
|
||||
|
||||
FirstRunWizard.scene.title="Choose a Target Scene"
|
||||
FirstRunWizard.scene.subtitle="Which OBS scene should become active when your chosen window comes into focus?"
|
||||
@@ -2696,6 +2803,26 @@ FirstRunWizard.review.summary="<b>Macro name:</b> Window -> %1<br><br><b>Conditi
|
||||
FirstRunWizard.review.errorTitle="Macro Creation Failed"
|
||||
FirstRunWizard.review.errorBody="The macro could not be created automatically.\n\nYou can create it manually on the Macros tab:\n Condition: Window -> contains \"%1\"\n Action: Switch scene -> \"%2\""
|
||||
|
||||
FirstRunWizard.seqTrigger.title="Choose a Trigger Scene"
|
||||
FirstRunWizard.seqTrigger.subtitle="The sequence will start when you have been on this scene for the specified duration."
|
||||
FirstRunWizard.seqTrigger.scene="Trigger scene:{{scene}}"
|
||||
FirstRunWizard.seqTrigger.delay="Start sequence after{{duration}}"
|
||||
|
||||
FirstRunWizard.seqScenes.title="Build Your Sequence"
|
||||
FirstRunWizard.seqScenes.subtitle="Add at least two scenes. The plugin will switch through them in order, waiting the configured time between each switch."
|
||||
FirstRunWizard.seqScenes.triggerInfo="When on \"%1\" for %2, switch to:"
|
||||
FirstRunWizard.seqScenes.addScene="Add scene"
|
||||
FirstRunWizard.seqScenes.removeTooltip="Remove this scene from the sequence"
|
||||
FirstRunWizard.seqScenes.switchTo="Switch to{{scene}}"
|
||||
FirstRunWizard.seqScenes.thenWait="then wait{{duration}}"
|
||||
|
||||
FirstRunWizard.seqReview.title="Review Your Sequence"
|
||||
FirstRunWizard.seqReview.subtitle="Click Back to make changes, or Finish to create the macro."
|
||||
FirstRunWizard.seqReview.trigger="When on <b>%1</b> for at least <b>%2</b>:"
|
||||
FirstRunWizard.seqReview.step="After %1, switch to %2"
|
||||
FirstRunWizard.seqReview.errorTitle="Macro Creation Failed"
|
||||
FirstRunWizard.seqReview.errorBody="The sequence macro could not be created. Please check the OBS log for details."
|
||||
|
||||
FirstRunWizard.done.title="You're all set!"
|
||||
FirstRunWizard.done.subtitle="Your first macro has been created and is now active."
|
||||
FirstRunWizard.done.body="<p>You can view and edit it on the <b>Macros</b> tab of the Advanced Scene Switcher dialog.</p><p>To learn more:</p><ul><li><a href=\"https://github.com/WarmUpTill/SceneSwitcher/wiki\">Plugin wiki</a></li><li><a href=\"https://obsproject.com/forum/resources/automatic-scene-switching.395/\">OBS forum thread</a></li></ul>"
|
||||
|
||||
@@ -312,8 +312,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Espere hasta que se comp
|
||||
AdvSceneSwitcher.action.wait="Esperar"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="fijo"
|
||||
AdvSceneSwitcher.action.wait.type.random="aleatorio"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Espere {{waitType}} de {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Espere {{waitType}} de {{duration}} a {{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Espere {{waitType}} de {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Espere {{waitType}} de {{duration}} a {{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Audio"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Silencio"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Activar silencio"
|
||||
|
||||
@@ -257,10 +257,10 @@ AdvSceneSwitcher.condition.video.layout.modelPath="Données du modèle (classifi
|
||||
AdvSceneSwitcher.condition.video.layout.minNeighbor="Nombre minimum de voisins :{{minNeighbors}}"
|
||||
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}Réduire la charge CPU en effectuant la vérification uniquement toutes les{{throttleCount}}millisecondes"
|
||||
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="Effectuer la vérification uniquement dans la zone"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Vérifier la couleur du texte :{{textColor}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Vérifier la couleur du texte :{{color}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrTextType="Vérifier le type de texte :{{textType}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrLanguage="Vérifier la langue :{{languageCode}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="Vérifier la couleur :{{color}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="Vérifier la couleur :{{color}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="Taille minimale :"
|
||||
AdvSceneSwitcher.condition.video.maxSize="Taille maximale :"
|
||||
AdvSceneSwitcher.condition.video.selectArea="Sélectionner la zone"
|
||||
@@ -460,8 +460,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Attendre que la transiti
|
||||
AdvSceneSwitcher.action.wait="Attendre"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="fixe"
|
||||
AdvSceneSwitcher.action.wait.type.random="aléatoire"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Attendre pour{{waitType}}une durée de{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Attendre pour{{waitType}}une durée de{{duration}}à{{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Attendre pour{{waitType}}une durée de{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Attendre pour{{waitType}}une durée de{{duration}}à{{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Audio"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Muet"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Activer le son"
|
||||
|
||||
@@ -414,12 +414,12 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="最小近傍数:{{minNeighb
|
||||
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}:{{throttleCount}}ミリ秒ごとにのみパフォーマンスチェックを実行することでCPU負荷を軽減します"
|
||||
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="エリア内のみチェックを実施"
|
||||
; AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="テキストカラーの確認:{{textColor}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="テキストカラーの確認:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrTextType="テキストタイプの確認:{{textType}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrBaseDir="Tesseractベースディレクトリ:{{tesseractBaseDir}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrLanguage="言語の確認:{{languageCode}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrConfig="設定ファイル:{{configFile}}{{openConfigFile}}{{reloadConfig}}{{configFileHint}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="カラーを確認してください:{{color}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="カラーを確認してください:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="最小サイズ:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="最大サイズ:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="範囲指定"
|
||||
@@ -800,8 +800,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="目的のシーンへの
|
||||
; AdvSceneSwitcher.action.wait="Wait"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="指定"
|
||||
AdvSceneSwitcher.action.wait.type.random="ランダム"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="{{waitType}}期間{{duration}}待機します"
|
||||
AdvSceneSwitcher.action.wait.entry.random="{{duration}}から{{duration2}}までの{{waitType}}期間待ちます"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="{{waitType}}期間{{duration}}待機します"
|
||||
AdvSceneSwitcher.action.wait.layout.random="{{duration}}から{{duration2}}までの{{waitType}}期間待ちます"
|
||||
AdvSceneSwitcher.action.audio="音声"
|
||||
AdvSceneSwitcher.action.audio.type.mute="ミュート"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="ミュート解除"
|
||||
@@ -830,7 +830,6 @@ AdvSceneSwitcher.action.recording.type.changeOutputFolder="出力フォルダの
|
||||
AdvSceneSwitcher.action.recording.type.changeOutputFileFormat="ファイル名の書式変更"
|
||||
AdvSceneSwitcher.action.recording.pause.hint="録画設定によっては録画を一時停止できない場合がありますのでご注意ください"
|
||||
AdvSceneSwitcher.action.recording.split.hint="まずOBS設定で自動ファイル分割を有効にしてください!"
|
||||
; AdvSceneSwitcher.action.recording.entry="{{actions}}{{recordFolder}}{{recordFileFormat}}{{pauseHint}}{{splitHint}}"
|
||||
AdvSceneSwitcher.action.replay="リプレイバッファー"
|
||||
AdvSceneSwitcher.action.replay.saveWarn="警告: 頻繁に保存しすぎると、リプレイ バッファーが実際には保存されなくなる可能性があります。"
|
||||
AdvSceneSwitcher.action.replay.durationWarn="注意: 最大リプレイ時間の変更は、次回再生バッファを開始したときから適用されます!"
|
||||
|
||||
@@ -371,10 +371,10 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="Vizinhos mínimos:{{minNeig
|
||||
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}Reduzir carga de CPU realizando a verificação apenas a cada {{throttleCount}} milissegundos"
|
||||
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="Realizar verificação apenas na área"
|
||||
AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Verificar cor do texto:{{textColor}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="Verificar cor do texto:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrTextType="Verificar tipo de texto:{{textType}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrLanguage="Verificar idioma:{{languageCode}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="Verificar cor:{{color}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="Verificar cor:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="Tamanho mínimo:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="Tamanho máximo:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="Selecionar área"
|
||||
@@ -718,8 +718,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Aguardar até que a tran
|
||||
AdvSceneSwitcher.action.wait="Aguardar"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="fixo"
|
||||
AdvSceneSwitcher.action.wait.type.random="aleatório"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Aguardando por{{waitType}}duração de{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Aguardando por{{waitType}}duração de{{duration}}para{{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Aguardando por{{waitType}}duração de{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Aguardando por{{waitType}}duração de{{duration}}para{{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Áudio"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Silenciar"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Ativar som"
|
||||
|
||||
@@ -114,8 +114,8 @@ AdvSceneSwitcher.action.scene="Переключить сцену"
|
||||
AdvSceneSwitcher.action.wait="Подождать"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="фиксированный"
|
||||
AdvSceneSwitcher.action.wait.type.random="случайный"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Ожидать {{waitType}} в течении {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Ожидание {{waitType}} длительностью от {{duration}} до {{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Ожидать {{waitType}} в течении {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Ожидание {{waitType}} длительностью от {{duration}} до {{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Аудио"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Замьютить"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Размьютить"
|
||||
|
||||
@@ -249,8 +249,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="Hedef sahneye geçiş ta
|
||||
AdvSceneSwitcher.action.wait="Bekle"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="sabit"
|
||||
AdvSceneSwitcher.action.wait.type.random="rastgele"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="Şunu bekle {{waitType}} süre {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="Şunu bekle {{waitType}} bu aralıktan {{duration}} buna {{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="Şunu bekle {{waitType}} süre {{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="Şunu bekle {{waitType}} bu aralıktan {{duration}} buna {{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="Ses"
|
||||
AdvSceneSwitcher.action.audio.type.mute="Sessiz"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="Ses açmak"
|
||||
|
||||
@@ -395,10 +395,10 @@ AdvSceneSwitcher.condition.video.layout.minNeighbor="最小区域: {{minNeighbor
|
||||
AdvSceneSwitcher.condition.video.layout.throttle="{{throttleEnable}}每隔{{throttleCount}} 毫秒,才执行一次检查,从而减少 CPU 负载."
|
||||
AdvSceneSwitcher.condition.video.layout.checkAreaEnable="仅在区域内执行检查"
|
||||
AdvSceneSwitcher.condition.video.layout.checkArea="{{checkAreaEnable}}{{checkArea}}{{selectArea}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="文本检查颜色:{{textColor}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrColorPick="文本检查颜色:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrTextType="文本检查类型:{{textType}}"
|
||||
AdvSceneSwitcher.condition.video.layout.ocrLanguage="语言检查:{{languageCode}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="颜色检查:{{color}}{{selectColor}}"
|
||||
AdvSceneSwitcher.condition.video.layout.color="颜色检查:{{color}}"
|
||||
AdvSceneSwitcher.condition.video.minSize="最小尺寸:"
|
||||
AdvSceneSwitcher.condition.video.maxSize="最大尺寸:"
|
||||
AdvSceneSwitcher.condition.video.selectArea="选择区域"
|
||||
@@ -778,8 +778,8 @@ AdvSceneSwitcher.action.scene.blockUntilTransitionDone="等待转场到目标场
|
||||
AdvSceneSwitcher.action.wait="等待"
|
||||
AdvSceneSwitcher.action.wait.type.fixed="固定"
|
||||
AdvSceneSwitcher.action.wait.type.random="随机"
|
||||
AdvSceneSwitcher.action.wait.entry.fixed="等待时间{{waitType}}{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.entry.random="等待时间{{waitType}}{{duration}} 到 {{duration2}}"
|
||||
AdvSceneSwitcher.action.wait.layout.fixed="等待时间{{waitType}}{{duration}}"
|
||||
AdvSceneSwitcher.action.wait.layout.random="等待时间{{waitType}}{{duration}} 到 {{duration2}}"
|
||||
AdvSceneSwitcher.action.audio="音频"
|
||||
AdvSceneSwitcher.action.audio.type.mute="静音"
|
||||
AdvSceneSwitcher.action.audio.type.unmute="取消静音"
|
||||
|
||||
BIN
data/res/speech/ggml-tiny-q8_0.bin
Normal file
BIN
data/res/speech/ggml-tiny-q8_0.bin
Normal file
Binary file not shown.
1
deps/date
vendored
1
deps/date
vendored
Submodule deps/date deleted from 5bdb7e6f31
2
deps/opencv
vendored
2
deps/opencv
vendored
Submodule deps/opencv updated: 71d3237a09...40738fb16c
1
deps/opencv_contrib
vendored
Submodule
1
deps/opencv_contrib
vendored
Submodule
Submodule deps/opencv_contrib added at 755e50675d
1
deps/whisper.cpp
vendored
Submodule
1
deps/whisper.cpp
vendored
Submodule
Submodule deps/whisper.cpp added at 306c88f4d1
@@ -1,7 +1,4 @@
|
||||
#pragma once
|
||||
#include "macro-segment-list.hpp"
|
||||
#include "condition-logic.hpp"
|
||||
#include "log-helper.hpp"
|
||||
|
||||
#include <ui_advanced-scene-switcher.h>
|
||||
|
||||
@@ -117,6 +114,8 @@ public slots:
|
||||
void RenameSelectedMacro();
|
||||
void ExportMacros() const;
|
||||
void ImportMacros();
|
||||
void PauseSelectedMacros();
|
||||
void UnpauseSelectedMacros();
|
||||
void HighlightOnChange() const;
|
||||
void on_macroSettings_clicked();
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
#include "filter-combo-box.hpp"
|
||||
#include "first-run-wizard.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-search.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "source-helpers.hpp"
|
||||
#include "splitter-helpers.hpp"
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "version.h"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
@@ -452,7 +454,7 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
|
||||
}
|
||||
|
||||
bool wasSkipped = false;
|
||||
auto macro = FirstRunWizard::ShowWizard(this, &wasSkipped);
|
||||
auto macro = wiz::FirstRunWizard::ShowWizard(this, &wasSkipped);
|
||||
if (macro) {
|
||||
renameMacroIfNecessary(macro);
|
||||
QTimer::singleShot(0, this,
|
||||
@@ -466,7 +468,7 @@ void AdvSceneSwitcher::CheckFirstTimeSetup()
|
||||
|
||||
void AdvSceneSwitcher::on_openSetupWizard_clicked()
|
||||
{
|
||||
auto macro = FirstRunWizard::ShowWizard(this);
|
||||
auto macro = wiz::FirstRunWizard::ShowWizard(this);
|
||||
if (!macro) {
|
||||
return;
|
||||
}
|
||||
|
||||
196
lib/macro/macro-action-loop.cpp
Normal file
196
lib/macro/macro-action-loop.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
#include "macro-action-loop.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
const std::string MacroActionLoop::id = "loop";
|
||||
|
||||
bool MacroActionLoop::_registered = MacroActionFactory::Register(
|
||||
MacroActionLoop::id,
|
||||
{MacroActionLoop::Create, MacroActionLoopEdit::Create,
|
||||
"AdvSceneSwitcher.action.loop"});
|
||||
|
||||
bool MacroActionLoop::PerformAction()
|
||||
{
|
||||
int iterations = 0;
|
||||
const int maxIterations = _maxIterations;
|
||||
Macro *parentMacro = GetMacro();
|
||||
|
||||
if (maxIterations <= 0) {
|
||||
blog(LOG_WARNING,
|
||||
"loop action has invalid max iterations value of %d",
|
||||
maxIterations);
|
||||
return true;
|
||||
}
|
||||
|
||||
while (_loopMacro->CheckConditions()) {
|
||||
if (iterations >= maxIterations) {
|
||||
blog(LOG_WARNING,
|
||||
"loop action reached iteration limit of %d",
|
||||
maxIterations);
|
||||
break;
|
||||
}
|
||||
ablog(LOG_INFO, "loop iteration %d", iterations);
|
||||
if (!_loopMacro->PerformActions(true, false, true)) {
|
||||
break;
|
||||
}
|
||||
++iterations;
|
||||
if (parentMacro && parentMacro->GetStop()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ablog(LOG_INFO, "loop completed after %d iteration(s)", iterations);
|
||||
SetTempVarValue("count", std::to_string(iterations));
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionLoop::LogAction() const
|
||||
{
|
||||
ablog(LOG_INFO, "running loop (max %d iterations)",
|
||||
_maxIterations.GetValue());
|
||||
}
|
||||
|
||||
bool MacroActionLoop::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
_maxIterations.Save(obj, "maxIterations");
|
||||
OBSDataAutoRelease loopMacroData = obs_data_create();
|
||||
_loopMacro->Save(loopMacroData);
|
||||
obs_data_set_obj(obj, "loopMacro", loopMacroData);
|
||||
obs_data_set_int(obj, "customWidgetHeight", _customWidgetHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionLoop::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_maxIterations.Load(obj, "maxIterations");
|
||||
if (obs_data_has_user_value(obj, "loopMacro")) {
|
||||
OBSDataAutoRelease loopMacroData =
|
||||
obs_data_get_obj(obj, "loopMacro");
|
||||
_loopMacro = std::make_shared<Macro>();
|
||||
_loopMacro->Load(loopMacroData);
|
||||
}
|
||||
_customWidgetHeight = obs_data_get_int(obj, "customWidgetHeight");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionLoop::PostLoad()
|
||||
{
|
||||
MacroAction::PostLoad();
|
||||
_loopMacro->PostLoad();
|
||||
_loopMacro->SetActionTriggerMode(Macro::ActionTriggerMode::ALWAYS);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionLoop::Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionLoop>(m);
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionLoop::Copy() const
|
||||
{
|
||||
auto copy = std::make_shared<MacroActionLoop>(*this);
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
_loopMacro->Save(data);
|
||||
|
||||
copy->_loopMacro = std::make_shared<Macro>();
|
||||
copy->_loopMacro->Load(data);
|
||||
copy->_loopMacro->PostLoad();
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
void MacroActionLoop::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_maxIterations.ResolveVariables();
|
||||
for (auto &action : _loopMacro->Actions()) {
|
||||
action->ResolveVariablesToFixedValues();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionLoop::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
AddTempvar("count",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.loop.count"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.loop.count.description"));
|
||||
}
|
||||
|
||||
MacroActionLoopEdit::MacroActionLoopEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionLoop> entryData)
|
||||
: ResizableWidget(parent),
|
||||
_macroEdit(new MacroEdit(
|
||||
this, QStringList()
|
||||
<< "AdvSceneSwitcher.action.loop.conditionHelp"
|
||||
<< "AdvSceneSwitcher.action.loop.actionHelp"
|
||||
<< "")),
|
||||
_maxIterations(new VariableSpinBox())
|
||||
{
|
||||
_maxIterations->setMinimum(1);
|
||||
_maxIterations->setMaximum(10000000);
|
||||
|
||||
QWidget::connect(
|
||||
_maxIterations,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
|
||||
this, SLOT(MaxIterationsChanged(const NumberVariable<int> &)));
|
||||
|
||||
auto controlsLayout = new QHBoxLayout();
|
||||
controlsLayout->addWidget(new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.action.loop.maxIterations")));
|
||||
controlsLayout->addWidget(_maxIterations);
|
||||
controlsLayout->addStretch();
|
||||
|
||||
auto layout = new QVBoxLayout();
|
||||
layout->addLayout(controlsLayout);
|
||||
layout->addWidget(_macroEdit);
|
||||
setLayout(layout);
|
||||
|
||||
_macroEdit->HideElseSection();
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
MacroActionLoopEdit::~MacroActionLoopEdit()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
_entryData->_customWidgetHeight = GetCustomHeight();
|
||||
_macroEdit->SetMacro({});
|
||||
}
|
||||
|
||||
void MacroActionLoopEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_maxIterations->SetValue(_entryData->_maxIterations);
|
||||
_macroEdit->SetMacro(_entryData->_loopMacro);
|
||||
|
||||
if (_macroEdit->IsEmpty()) {
|
||||
_macroEdit->ShowAllMacroSections();
|
||||
_entryData->_customWidgetHeight = 600;
|
||||
}
|
||||
SetResizingEnabled(true);
|
||||
SetCustomHeight(_entryData->_customWidgetHeight);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroActionLoopEdit::MaxIterationsChanged(const NumberVariable<int> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_maxIterations = value;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
64
lib/macro/macro-action-loop.hpp
Normal file
64
lib/macro/macro-action-loop.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-edit.hpp"
|
||||
#include "resizable-widget.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroActionLoop : public MacroAction {
|
||||
public:
|
||||
MacroActionLoop(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
bool PostLoad();
|
||||
std::string GetId() const { return id; }
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
void ResolveVariablesToFixedValues();
|
||||
|
||||
std::shared_ptr<Macro> _loopMacro = std::make_shared<Macro>();
|
||||
IntVariable _maxIterations = 100;
|
||||
int _customWidgetHeight = 0;
|
||||
|
||||
private:
|
||||
void SetupTempVars();
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionLoopEdit : public ResizableWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroActionLoopEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionLoop> entryData = nullptr);
|
||||
~MacroActionLoopEdit();
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionLoopEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroActionLoop>(action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void MaxIterationsChanged(const NumberVariable<int> &value);
|
||||
|
||||
private:
|
||||
MacroEdit *_macroEdit;
|
||||
VariableSpinBox *_maxIterations;
|
||||
|
||||
std::shared_ptr<MacroActionLoop> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "macro-action-macro.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
#include "help-icon.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -447,7 +447,19 @@ bool MacroActionVariable::PerformAction()
|
||||
if (!value.has_value()) {
|
||||
return true;
|
||||
}
|
||||
var->SetValue(*value);
|
||||
|
||||
if (!_jsonQueryExtractSingle) {
|
||||
var->SetValue(*value);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto extracted = ExtractSingleJsonArrayElement(*value);
|
||||
if (extracted.has_value()) {
|
||||
var->SetValue(*extracted);
|
||||
} else {
|
||||
var->SetValue(*value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
case Action::ARRAY_JSON: {
|
||||
@@ -551,6 +563,8 @@ bool MacroActionVariable::Save(obs_data_t *obj) const
|
||||
obs_data_set_bool(obj, "allowRepeatValues", _allowRepeatValues);
|
||||
_jsonQuery.Save(obj, "jsonQuery");
|
||||
_jsonIndex.Save(obj, "jsonIndex");
|
||||
obs_data_set_bool(obj, "jsonQueryExtractSingle",
|
||||
_jsonQueryExtractSingle);
|
||||
|
||||
obs_data_set_int(obj, "version", 1);
|
||||
|
||||
@@ -611,6 +625,8 @@ bool MacroActionVariable::Load(obs_data_t *obj)
|
||||
_allowRepeatValues = obs_data_get_bool(obj, "allowRepeatValues");
|
||||
_jsonQuery.Load(obj, "jsonQuery");
|
||||
_jsonIndex.Load(obj, "jsonIndex");
|
||||
_jsonQueryExtractSingle =
|
||||
obs_data_get_bool(obj, "jsonQueryExtractSingle");
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -904,6 +920,15 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
"AdvSceneSwitcher.action.variable.type.queryJson.info"),
|
||||
this)),
|
||||
_jsonIndex(new VariableSpinBox(this)),
|
||||
_jsonExtractSingle(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.type.queryJson.extractSingle"),
|
||||
this)),
|
||||
_jsonExtractSingleHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.type.queryJson.extractSingle.info"),
|
||||
this)),
|
||||
_jsonQueryLayout(new QVBoxLayout()),
|
||||
_entryLayout(new QHBoxLayout())
|
||||
{
|
||||
_numValue->setMinimum(-9999999999);
|
||||
@@ -935,6 +960,11 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
_randomNumberEnd->setMaximum(9999999999);
|
||||
_randomValues->SetMaxStringSize(99999999);
|
||||
_jsonIndex->setMaximum(999);
|
||||
auto jsonExtractSingleLayout = new QHBoxLayout();
|
||||
jsonExtractSingleLayout->addWidget(_jsonExtractSingle);
|
||||
jsonExtractSingleLayout->addWidget(_jsonExtractSingleHelp);
|
||||
jsonExtractSingleLayout->addStretch();
|
||||
_jsonQueryLayout->addLayout(jsonExtractSingleLayout);
|
||||
|
||||
QWidget::connect(_variables, SIGNAL(SelectionChanged(const QString &)),
|
||||
this, SLOT(VariableChanged(const QString &)));
|
||||
@@ -1033,6 +1063,8 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
_jsonIndex,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
|
||||
this, SLOT(JsonIndexChanged(const NumberVariable<int> &)));
|
||||
QWidget::connect(_jsonExtractSingle, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(JsonQueryExtractSingleChanged(int)));
|
||||
|
||||
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{variables}}", _variables},
|
||||
@@ -1109,6 +1141,7 @@ MacroActionVariableEdit::MacroActionVariableEdit(
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->addLayout(_entryLayout);
|
||||
layout->addLayout(_jsonQueryLayout);
|
||||
layout->addLayout(_substringLayout);
|
||||
layout->addWidget(_segmentValueStatus);
|
||||
layout->addWidget(_segmentValue);
|
||||
@@ -1180,6 +1213,7 @@ void MacroActionVariableEdit::UpdateEntryData()
|
||||
_randomValues->SetStringList(_entryData->_randomValues);
|
||||
_jsonQuery->setText(_entryData->_jsonQuery);
|
||||
_jsonIndex->SetValue(_entryData->_jsonIndex);
|
||||
_jsonExtractSingle->setChecked(_entryData->_jsonQueryExtractSingle);
|
||||
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
@@ -1545,6 +1579,12 @@ void MacroActionVariableEdit::JsonIndexChanged(const NumberVariable<int> &value)
|
||||
_entryData->_jsonIndex = value;
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::JsonQueryExtractSingleChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_jsonQueryExtractSingle = value;
|
||||
}
|
||||
|
||||
void MacroActionVariableEdit::SetWidgetVisibility()
|
||||
{
|
||||
if (!_entryData) {
|
||||
@@ -1593,6 +1633,10 @@ void MacroActionVariableEdit::SetWidgetVisibility()
|
||||
MacroActionVariable::Action::TRUNCATE) {
|
||||
layoutString = obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.layout.truncate");
|
||||
} else if (_entryData->_action ==
|
||||
MacroActionVariable::Action::COPY_VAR) {
|
||||
layoutString = obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.layout.copy");
|
||||
} else {
|
||||
layoutString = obs_module_text(
|
||||
"AdvSceneSwitcher.action.variable.layout.other");
|
||||
@@ -1740,6 +1784,9 @@ void MacroActionVariableEdit::SetWidgetVisibility()
|
||||
MacroActionVariable::Action::QUERY_JSON);
|
||||
_jsonIndex->setVisible(_entryData->_action ==
|
||||
MacroActionVariable::Action::ARRAY_JSON);
|
||||
SetLayoutVisible(_jsonQueryLayout,
|
||||
_entryData->_action ==
|
||||
MacroActionVariable::Action::QUERY_JSON);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
@@ -116,6 +116,7 @@ public:
|
||||
|
||||
StringVariable _jsonQuery = "$.some.nested.value";
|
||||
IntVariable _jsonIndex = 0;
|
||||
bool _jsonQueryExtractSingle = true;
|
||||
|
||||
private:
|
||||
void DecrementCurrentSegmentVariableRef();
|
||||
@@ -187,6 +188,7 @@ private slots:
|
||||
void AllowRepeatValuesChanged(int);
|
||||
void JsonQueryChanged();
|
||||
void JsonIndexChanged(const NumberVariable<int> &);
|
||||
void JsonQueryExtractSingleChanged(int);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
@@ -239,8 +241,11 @@ private:
|
||||
QCheckBox *_allowRepeatValues;
|
||||
QVBoxLayout *_randomValueLayout;
|
||||
VariableLineEdit *_jsonQuery;
|
||||
QLabel *_jsonQueryHelp;
|
||||
HelpIcon *_jsonQueryHelp;
|
||||
VariableSpinBox *_jsonIndex;
|
||||
QCheckBox *_jsonExtractSingle;
|
||||
HelpIcon *_jsonExtractSingleHelp;
|
||||
QVBoxLayout *_jsonQueryLayout;
|
||||
QHBoxLayout *_entryLayout;
|
||||
|
||||
std::shared_ptr<MacroActionVariable> _entryData;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "macro-condition-edit.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "condition-logic.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "macro-condition-macro.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "macro-condition.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
MacroCondition::MacroCondition(Macro *m, bool supportsVariableValue)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "macro-dock-settings.hpp"
|
||||
#include "macro-dock.hpp"
|
||||
|
||||
#include "macro-dock-window.hpp"
|
||||
#include "macro-dock.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "variable-string.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <util/platform.h>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
#include "macro-edit.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
#include "cursor-shape-changer.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-action-macro.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-segment-copy-paste.hpp"
|
||||
#include "macro-segment-list.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "math-helpers.hpp"
|
||||
#include "name-dialog.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "splitter-helpers.hpp"
|
||||
#include "tab-helpers.hpp"
|
||||
@@ -1850,6 +1853,13 @@ bool MacroEdit::IsEmpty() const
|
||||
ui->elseActionsList->IsEmpty();
|
||||
}
|
||||
|
||||
void MacroEdit::HideElseSection() const
|
||||
{
|
||||
ui->toggleElseActions->setVisible(false);
|
||||
ui->macroElseActions->setMaximumHeight(0);
|
||||
ui->macroElseActionSplitter->handle(1)->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
void MacroEdit::ShowAllMacroSections()
|
||||
{
|
||||
if (!ElseSectionIsVisible()) {
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
void PasteMacroSegment();
|
||||
bool IsEmpty() const;
|
||||
void ShowAllMacroSections();
|
||||
void HideElseSection() const;
|
||||
|
||||
private slots:
|
||||
void on_conditionAdd_clicked();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -108,7 +108,7 @@ void MacroRunButton::Pressed()
|
||||
const bool abortMacro = DisplayMessage(
|
||||
err.arg(QString::fromStdString(macro->Name())), true);
|
||||
if (abortMacro) {
|
||||
macro->Stop();
|
||||
macro->SignalStop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "macro-segment-copy-paste.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "condition-logic.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include <QShortcut>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "macro-segment.hpp"
|
||||
|
||||
#include "macro.hpp"
|
||||
#include "mouse-wheel-guard.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
@@ -8,10 +9,13 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPushButton>
|
||||
#include <QScrollBar>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
#include "sync-helpers.hpp"
|
||||
#include "temp-variable.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QFrame>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QTimer>
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
class QVBoxLayout;
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "macro-selection.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "macro-export-import-dialog.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-search.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro-tree.hpp"
|
||||
#include "macro.hpp"
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "version.h"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QMenu>
|
||||
|
||||
@@ -743,6 +745,17 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
remove->setDisabled(ui->macros->SelectionEmpty());
|
||||
menu.addSeparator();
|
||||
|
||||
auto pause = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.pause"), this,
|
||||
&AdvSceneSwitcher::PauseSelectedMacros);
|
||||
pause->setDisabled(ui->macros->SelectionEmpty());
|
||||
|
||||
auto unpause = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.unpause"), this,
|
||||
&AdvSceneSwitcher::UnpauseSelectedMacros);
|
||||
unpause->setDisabled(ui->macros->SelectionEmpty());
|
||||
menu.addSeparator();
|
||||
|
||||
auto group = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.group"), ui->macros,
|
||||
&MacroTree::GroupSelectedItems);
|
||||
@@ -803,6 +816,48 @@ void AdvSceneSwitcher::CopyMacro()
|
||||
MacroSignalManager::Instance()->Add(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::PauseSelectedMacros()
|
||||
{
|
||||
auto selectedMacros = GetSelectedMacros();
|
||||
if (selectedMacros.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
for (const auto ¯o : selectedMacros) {
|
||||
if (macro->IsGroup()) {
|
||||
for (const auto &subitem :
|
||||
GetGroupMacroEntries(macro.get())) {
|
||||
subitem->SetPaused(true);
|
||||
}
|
||||
} else {
|
||||
macro->SetPaused(true);
|
||||
}
|
||||
}
|
||||
ui->macros->UpdateRunningStates();
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::UnpauseSelectedMacros()
|
||||
{
|
||||
auto selectedMacros = GetSelectedMacros();
|
||||
if (selectedMacros.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
for (const auto ¯o : selectedMacros) {
|
||||
if (macro->IsGroup()) {
|
||||
for (const auto &subitem :
|
||||
GetGroupMacroEntries(macro.get())) {
|
||||
subitem->SetPaused(false);
|
||||
}
|
||||
} else {
|
||||
macro->SetPaused(false);
|
||||
}
|
||||
}
|
||||
ui->macros->UpdateRunningStates();
|
||||
}
|
||||
|
||||
bool MacroTabIsInFocus()
|
||||
{
|
||||
return AdvSceneSwitcher::window &&
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
#include "macro-tree.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-search.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <obs.h>
|
||||
#include <string>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSpacerItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QSpacerItem>
|
||||
#include <QStylePainter>
|
||||
#include <QToolTip>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <string>
|
||||
|
||||
Q_DECLARE_METATYPE(std::shared_ptr<advss::Macro>);
|
||||
|
||||
@@ -977,6 +981,17 @@ void MacroTree::UpdateWidgets(bool force)
|
||||
}
|
||||
}
|
||||
|
||||
void MacroTree::UpdateRunningStates()
|
||||
{
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
for (int i = 0; i < (int)mtm->_macros.size(); i++) {
|
||||
auto widget = GetItemWidget(i);
|
||||
if (widget) {
|
||||
widget->UpdateRunning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline void MoveItem(std::deque<std::shared_ptr<Macro>> &items,
|
||||
std::shared_ptr<Macro> &item, int to)
|
||||
{
|
||||
|
||||
@@ -145,6 +145,7 @@ public:
|
||||
void ExpandGroup(std::shared_ptr<Macro> item) const;
|
||||
void CollapseGroup(std::shared_ptr<Macro> item) const;
|
||||
void RefreshFilter();
|
||||
void UpdateRunningStates();
|
||||
|
||||
public slots:
|
||||
void GroupSelectedItems();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro-condition-factory.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
@@ -7,12 +9,14 @@
|
||||
#include "splitter-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QMainWindow>
|
||||
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#undef max
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QAction>
|
||||
#include <QMainWindow>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace advss {
|
||||
@@ -552,6 +556,12 @@ Macro::PauseStateSaveBehavior Macro::GetPauseStateSaveBehavior() const
|
||||
return _pauseSaveBehavior;
|
||||
}
|
||||
|
||||
void Macro::SignalStop()
|
||||
{
|
||||
_stop = true;
|
||||
GetMacroWaitCV().notify_all();
|
||||
}
|
||||
|
||||
void Macro::Stop()
|
||||
{
|
||||
_stop = true;
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "macro-action.hpp"
|
||||
#include "macro-condition.hpp"
|
||||
#include "macro-dock-settings.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-input.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
#include "variable-string.hpp"
|
||||
#include "temp-variable.hpp"
|
||||
|
||||
#include <future>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <obs.hpp>
|
||||
#include <obs-module-helper.hpp>
|
||||
|
||||
#include <QList>
|
||||
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -61,7 +60,7 @@ public:
|
||||
PauseStateSaveBehavior GetPauseStateSaveBehavior() const;
|
||||
|
||||
void Stop();
|
||||
void SignalStop() { _stop = true; }
|
||||
void SignalStop();
|
||||
bool GetStop() const { return _stop; }
|
||||
void ResetTimers();
|
||||
|
||||
@@ -225,12 +224,7 @@ private:
|
||||
MacroDockSettings _dockSettings;
|
||||
};
|
||||
|
||||
void LoadMacros(obs_data_t *obj);
|
||||
void SaveMacros(obs_data_t *obj);
|
||||
bool CheckMacros();
|
||||
bool RunMacros();
|
||||
void WaitForAllMacros();
|
||||
void InvalidateMacroTempVarValues();
|
||||
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval();
|
||||
|
||||
} // namespace advss
|
||||
|
||||
40
lib/pch.hpp
Normal file
40
lib/pch.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// OBS
|
||||
#include <obs-data.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs.hpp>
|
||||
|
||||
// Qt
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
// Standard library
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::deque<std::shared_ptr<Item>> queues;
|
||||
@@ -42,6 +45,7 @@ void ActionQueue::Save(obs_data_t *obj) const
|
||||
obs_data_set_string(obj, "name", _name.c_str());
|
||||
obs_data_set_bool(obj, "runOnStartup", _runOnStartup);
|
||||
obs_data_set_bool(obj, "resolveVariablesOnAdd", _resolveVariablesOnAdd);
|
||||
obs_data_set_bool(obj, "cloneVariableContext", _cloneVariableContext);
|
||||
}
|
||||
|
||||
void ActionQueue::Load(obs_data_t *obj)
|
||||
@@ -51,6 +55,7 @@ void ActionQueue::Load(obs_data_t *obj)
|
||||
_runOnStartup = obs_data_get_bool(obj, "runOnStartup");
|
||||
_resolveVariablesOnAdd =
|
||||
obs_data_get_bool(obj, "resolveVariablesOnAdd");
|
||||
_cloneVariableContext = obs_data_get_bool(obj, "cloneVariableContext");
|
||||
|
||||
if (_runOnStartup) {
|
||||
Start();
|
||||
@@ -111,9 +116,17 @@ void ActionQueue::Add(const std::shared_ptr<MacroAction> &action)
|
||||
copy->PostLoad();
|
||||
RunAndClearPostLoadSteps();
|
||||
copy->ResolveVariablesToFixedValues();
|
||||
_actions.emplace_back(copy);
|
||||
_actions.push_back({copy, {}});
|
||||
} else if (_cloneVariableContext) {
|
||||
auto copy = action->Copy();
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
action->Save(data);
|
||||
copy->Load(data);
|
||||
copy->PostLoad();
|
||||
RunAndClearPostLoadSteps();
|
||||
_actions.push_back({copy, CreateVariableContext()});
|
||||
} else {
|
||||
_actions.emplace_back(action);
|
||||
_actions.push_back({action, {}});
|
||||
}
|
||||
_cv.notify_all();
|
||||
}
|
||||
@@ -140,7 +153,7 @@ size_t ActionQueue::Size()
|
||||
|
||||
void ActionQueue::RunActions()
|
||||
{
|
||||
std::shared_ptr<MacroAction> action;
|
||||
QueueEntry entry;
|
||||
while (true) {
|
||||
{ // Grab next action to run
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
@@ -153,20 +166,25 @@ void ActionQueue::RunActions()
|
||||
if (_stop) {
|
||||
return;
|
||||
}
|
||||
action = _actions.front();
|
||||
entry = _actions.front();
|
||||
_actions.pop_front();
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
if (!entry.action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ActionLoggingEnabled()) {
|
||||
blog(LOG_INFO, "Performing action '%s' in queue '%s'",
|
||||
action->GetId().c_str(), _name.c_str());
|
||||
action->LogAction();
|
||||
entry.action->GetId().c_str(), _name.c_str());
|
||||
entry.action->LogAction();
|
||||
}
|
||||
action->PerformAction();
|
||||
|
||||
if (entry.context) {
|
||||
SetActiveVariableContext(&*entry.context);
|
||||
}
|
||||
entry.action->PerformAction();
|
||||
SetActiveVariableContext(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +202,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
|
||||
obs_module_text("AdvSceneSwitcher.actionQueues.clear"))),
|
||||
_runOnStartup(new QCheckBox()),
|
||||
_resolveVariablesOnAdd(new QCheckBox()),
|
||||
_cloneVariableContext(new QCheckBox()),
|
||||
_queue(settings)
|
||||
{
|
||||
QWidget::connect(_startStopToggle, SIGNAL(clicked()), this,
|
||||
@@ -192,6 +211,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
|
||||
|
||||
_runOnStartup->setChecked(settings._runOnStartup);
|
||||
_resolveVariablesOnAdd->setChecked(settings._resolveVariablesOnAdd);
|
||||
_cloneVariableContext->setChecked(settings._cloneVariableContext);
|
||||
UpdateLabels();
|
||||
|
||||
auto layout = new QGridLayout();
|
||||
@@ -221,6 +241,14 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
|
||||
_resolveVariablesOnAdd->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.actionQueues.resolveVariablesOnAdd"));
|
||||
++row;
|
||||
layout->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.actionQueues.cloneVariableContext")),
|
||||
row, 0);
|
||||
layout->addWidget(_cloneVariableContext, row, 1);
|
||||
_cloneVariableContext->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.actionQueues.cloneVariableContext.tooltip"));
|
||||
++row;
|
||||
layout->addWidget(_queueRunStatus, row, 0);
|
||||
layout->addWidget(_startStopToggle, row, 1);
|
||||
++row;
|
||||
@@ -250,6 +278,8 @@ bool ActionQueueSettingsDialog::AskForSettings(QWidget *parent,
|
||||
settings._runOnStartup = dialog._runOnStartup->isChecked();
|
||||
settings._resolveVariablesOnAdd =
|
||||
dialog._resolveVariablesOnAdd->isChecked();
|
||||
settings._cloneVariableContext =
|
||||
dialog._cloneVariableContext->isChecked();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
#include "item-selection-helpers.hpp"
|
||||
#include "macro-action.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <obs-data.h>
|
||||
#include <optional>
|
||||
#include <QCheckBox>
|
||||
#include <thread>
|
||||
|
||||
@@ -17,6 +19,11 @@ class ActionQueueSettingsDialog;
|
||||
class ActionQueue : public Item {
|
||||
using TimePoint = std::chrono::high_resolution_clock::time_point;
|
||||
|
||||
struct QueueEntry {
|
||||
std::shared_ptr<MacroAction> action;
|
||||
std::optional<VariableContext> context;
|
||||
};
|
||||
|
||||
public:
|
||||
ActionQueue();
|
||||
~ActionQueue();
|
||||
@@ -43,11 +50,12 @@ private:
|
||||
|
||||
bool _runOnStartup = true;
|
||||
bool _resolveVariablesOnAdd = true;
|
||||
bool _cloneVariableContext = false;
|
||||
std::atomic_bool _stop = {true};
|
||||
std::mutex _mutex;
|
||||
std::condition_variable _cv;
|
||||
std::thread _thread;
|
||||
std::deque<std::shared_ptr<MacroAction>> _actions;
|
||||
std::deque<QueueEntry> _actions;
|
||||
TimePoint _lastEmpty;
|
||||
|
||||
friend ActionQueueSelection;
|
||||
@@ -73,6 +81,7 @@ private:
|
||||
QPushButton *_clear;
|
||||
QCheckBox *_runOnStartup;
|
||||
QCheckBox *_resolveVariablesOnAdd;
|
||||
QCheckBox *_cloneVariableContext;
|
||||
|
||||
ActionQueue &_queue;
|
||||
};
|
||||
|
||||
425
lib/utils/first-run-wizard-audio.cpp
Normal file
425
lib/utils/first-run-wizard-audio.cpp
Normal file
@@ -0,0 +1,425 @@
|
||||
#include "first-run-wizard-audio.hpp"
|
||||
#include "first-run-wizard-helpers.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
namespace wiz {
|
||||
|
||||
// Builds the obs_data blob for an audio volume condition,
|
||||
// matching MacroConditionAudio::Save() output.
|
||||
//
|
||||
// Checks output volume (checkType 0) ABOVE (outputCondition 0) the given
|
||||
// threshold in dB, held for at least `duration`.
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 2 },
|
||||
// "id": "audio",
|
||||
// "logic": 0,
|
||||
// "durationModifier": {
|
||||
// "time_constraint": 1, // AT_LEAST
|
||||
// "seconds": <Duration::Save output>
|
||||
// },
|
||||
// "audioSource": { "type": 0, "name": "<source>" },
|
||||
// "monitor": 0,
|
||||
// "volume": { "value": 0.0, "type": 0 },
|
||||
// "syncOffset": { "value": 0, "type": 0 },
|
||||
// "balance": { "value": 0.5, "type": 0 },
|
||||
// "checkType": 0, // OUTPUT_VOLUME
|
||||
// "outputCondition": 0, // ABOVE
|
||||
// "volumeCondition": 0,
|
||||
// "useDb": true,
|
||||
// "volumeDB": { "value": <thresholdDb>, "type": 0 },
|
||||
// "version": 3
|
||||
// }
|
||||
static OBSDataAutoRelease buildAudioConditionData(const QString &sourceName,
|
||||
double thresholdDb,
|
||||
const Duration &duration)
|
||||
{
|
||||
OBSDataAutoRelease seg = obs_data_create();
|
||||
obs_data_set_bool(seg, "enabled", true);
|
||||
obs_data_set_int(seg, "version", 2);
|
||||
|
||||
OBSDataAutoRelease durMod = obs_data_create();
|
||||
obs_data_set_int(durMod, "time_constraint", 1);
|
||||
duration.Save(durMod, "seconds");
|
||||
|
||||
OBSDataAutoRelease audioSrc = obs_data_create();
|
||||
obs_data_set_int(audioSrc, "type", 0);
|
||||
obs_data_set_string(audioSrc, "name", sourceName.toUtf8().constData());
|
||||
|
||||
OBSDataAutoRelease volume = obs_data_create();
|
||||
obs_data_set_double(volume, "value", 0.0);
|
||||
obs_data_set_int(volume, "type", 0);
|
||||
|
||||
OBSDataAutoRelease syncOffset = obs_data_create();
|
||||
obs_data_set_int(syncOffset, "value", 0);
|
||||
obs_data_set_int(syncOffset, "type", 0);
|
||||
|
||||
OBSDataAutoRelease balance = obs_data_create();
|
||||
obs_data_set_double(balance, "value", 0.5);
|
||||
obs_data_set_int(balance, "type", 0);
|
||||
|
||||
OBSDataAutoRelease volumeDB = obs_data_create();
|
||||
obs_data_set_double(volumeDB, "value", thresholdDb);
|
||||
obs_data_set_int(volumeDB, "type", 0);
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_obj(data, "segmentSettings", seg);
|
||||
obs_data_set_string(data, "id", "audio");
|
||||
obs_data_set_int(data, "logic", 0);
|
||||
obs_data_set_obj(data, "durationModifier", durMod);
|
||||
obs_data_set_obj(data, "audioSource", audioSrc);
|
||||
obs_data_set_int(data, "monitor", 0);
|
||||
obs_data_set_obj(data, "volume", volume);
|
||||
obs_data_set_obj(data, "syncOffset", syncOffset);
|
||||
obs_data_set_obj(data, "balance", balance);
|
||||
obs_data_set_int(data, "checkType", 0);
|
||||
obs_data_set_int(data, "outputCondition", 0);
|
||||
obs_data_set_int(data, "volumeCondition", 0);
|
||||
obs_data_set_bool(data, "useDb", true);
|
||||
obs_data_set_obj(data, "volumeDB", volumeDB);
|
||||
obs_data_set_int(data, "version", 3);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Builds the obs_data blob for a scene-visibility action on the current scene,
|
||||
// matching MacroActionSceneVisibility::Save() output.
|
||||
//
|
||||
// sceneSelection type 3 = current scene.
|
||||
// action 0 = SHOW, action 1 = HIDE.
|
||||
static OBSDataAutoRelease buildVisibilityActionData(const QString &sourceName,
|
||||
int action)
|
||||
{
|
||||
OBSDataAutoRelease seg = obs_data_create();
|
||||
obs_data_set_bool(seg, "enabled", true);
|
||||
obs_data_set_int(seg, "version", 2);
|
||||
|
||||
OBSDataAutoRelease sceneSel = obs_data_create();
|
||||
obs_data_set_int(sceneSel, "type", 3); // current scene
|
||||
obs_data_set_string(sceneSel, "canvasSelection", "Main");
|
||||
|
||||
OBSDataAutoRelease itemSel = obs_data_create();
|
||||
obs_data_set_int(itemSel, "type", 0);
|
||||
obs_data_set_int(itemSel, "idxType", 0);
|
||||
obs_data_set_int(itemSel, "idx", 0);
|
||||
obs_data_set_string(itemSel, "item", sourceName.toUtf8().constData());
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_obj(data, "segmentSettings", seg);
|
||||
obs_data_set_string(data, "id", "scene_visibility");
|
||||
obs_data_set_obj(data, "sceneSelection", sceneSel);
|
||||
obs_data_set_obj(data, "sceneItemSelection", itemSel);
|
||||
obs_data_set_bool(data, "updateTransition", false);
|
||||
obs_data_set_int(data, "transitionType", 0);
|
||||
obs_data_set_string(data, "transition", "");
|
||||
obs_data_set_bool(data, "updateDuration", false);
|
||||
Duration().Save(data, "duration");
|
||||
obs_data_set_int(data, "action", action);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// AudioSourcePage
|
||||
// ===========================================================================
|
||||
|
||||
AudioSourcePage::AudioSourcePage(QWidget *parent)
|
||||
: QWizardPage(parent),
|
||||
_sourceCombo(new QComboBox(this)),
|
||||
_thresholdSpinbox(new QDoubleSpinBox(this)),
|
||||
_durationSelection(new DurationSelection(this, true, 0.0)),
|
||||
_volmeterLayout(new QVBoxLayout)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.audio.source.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.audio.source.subtitle"));
|
||||
|
||||
_sourceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
_thresholdSpinbox->setRange(-100.0, 0.0);
|
||||
_thresholdSpinbox->setValue(-24.0);
|
||||
_thresholdSpinbox->setDecimals(1);
|
||||
_thresholdSpinbox->setSingleStep(1.0);
|
||||
_thresholdSpinbox->setSuffix(" dB");
|
||||
|
||||
_durationSelection->SetDuration(Duration(1.0));
|
||||
|
||||
registerField("audioSourceName*", _sourceCombo, "currentText",
|
||||
SIGNAL(currentTextChanged(QString)));
|
||||
|
||||
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
|
||||
&AudioSourcePage::UpdateVolmeter);
|
||||
connect(_thresholdSpinbox,
|
||||
QOverload<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||||
&AudioSourcePage::SyncSliderFromSpinbox);
|
||||
|
||||
auto *sourceRow = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("FirstRunWizard.audio.source.sourceRow"),
|
||||
sourceRow, {{"{{source}}", _sourceCombo}}, false);
|
||||
|
||||
auto *thresholdRow = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text("FirstRunWizard.audio.source.thresholdRow"),
|
||||
thresholdRow,
|
||||
{{"{{threshold}}", _thresholdSpinbox},
|
||||
{"{{duration}}", _durationSelection}},
|
||||
false);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(sourceRow);
|
||||
layout->addLayout(thresholdRow);
|
||||
layout->addLayout(_volmeterLayout);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void AudioSourcePage::initializePage()
|
||||
{
|
||||
_sourceCombo->clear();
|
||||
PopulateAudioSelection(_sourceCombo, false);
|
||||
}
|
||||
|
||||
void AudioSourcePage::UpdateVolmeter()
|
||||
{
|
||||
delete _volControl;
|
||||
_volControl = nullptr;
|
||||
|
||||
OBSSourceAutoRelease source = obs_get_source_by_name(
|
||||
_sourceCombo->currentText().toUtf8().constData());
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
|
||||
_volControl = new VolControl(source.Get());
|
||||
_volmeterLayout->addWidget(_volControl);
|
||||
|
||||
connect(_volControl->GetSlider(), &DoubleSlider::DoubleValChanged, this,
|
||||
&AudioSourcePage::SyncSpinboxFromSlider);
|
||||
SyncSliderFromSpinbox();
|
||||
}
|
||||
|
||||
void AudioSourcePage::SyncSpinboxFromSlider()
|
||||
{
|
||||
if (!_volControl) {
|
||||
return;
|
||||
}
|
||||
const QSignalBlocker blocker(_thresholdSpinbox);
|
||||
_thresholdSpinbox->setValue(_volControl->GetSlider()->DoubleValue() -
|
||||
100.0);
|
||||
}
|
||||
|
||||
void AudioSourcePage::SyncSliderFromSpinbox()
|
||||
{
|
||||
if (!_volControl) {
|
||||
return;
|
||||
}
|
||||
const QSignalBlocker blocker(_volControl->GetSlider());
|
||||
_volControl->GetSlider()->SetDoubleVal(_thresholdSpinbox->value() +
|
||||
100.0);
|
||||
}
|
||||
|
||||
bool AudioSourcePage::isComplete() const
|
||||
{
|
||||
return _sourceCombo->count() > 0 &&
|
||||
!_sourceCombo->currentText().isEmpty();
|
||||
}
|
||||
|
||||
Duration AudioSourcePage::GetDuration() const
|
||||
{
|
||||
return _durationSelection->GetDuration();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// AudioTargetPage
|
||||
// ===========================================================================
|
||||
|
||||
AudioTargetPage::AudioTargetPage(QWidget *parent)
|
||||
: QWizardPage(parent),
|
||||
_sourceCombo(new QComboBox(this))
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.audio.target.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.audio.target.subtitle"));
|
||||
|
||||
_sourceCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
registerField("audioTargetSource*", _sourceCombo, "currentText",
|
||||
SIGNAL(currentTextChanged(QString)));
|
||||
|
||||
connect(_sourceCombo, &QComboBox::currentTextChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
|
||||
auto *row = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("FirstRunWizard.audio.target.sourceRow"),
|
||||
row, {{"{{source}}", _sourceCombo}}, false);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(row);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void AudioTargetPage::initializePage()
|
||||
{
|
||||
QStringList names;
|
||||
auto enumScenes = [](void *param, obs_source_t *src) -> bool {
|
||||
auto *list = reinterpret_cast<QStringList *>(param);
|
||||
obs_scene_t *scene = obs_scene_from_source(src);
|
||||
obs_scene_enum_items(
|
||||
scene,
|
||||
[](obs_scene_t *, obs_sceneitem_t *item,
|
||||
void *p) -> bool {
|
||||
auto *l = reinterpret_cast<QStringList *>(p);
|
||||
OBSSource s = obs_sceneitem_get_source(item);
|
||||
if (s) {
|
||||
*l << obs_source_get_name(s);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
list);
|
||||
return true;
|
||||
};
|
||||
obs_enum_scenes(enumScenes, &names);
|
||||
names.sort(Qt::CaseInsensitive);
|
||||
names.removeDuplicates();
|
||||
|
||||
const QString prev = _sourceCombo->currentText();
|
||||
_sourceCombo->clear();
|
||||
_sourceCombo->addItems(names);
|
||||
const int idx = _sourceCombo->findText(prev);
|
||||
if (idx >= 0) {
|
||||
_sourceCombo->setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioTargetPage::isComplete() const
|
||||
{
|
||||
return _sourceCombo->count() > 0 &&
|
||||
!_sourceCombo->currentText().isEmpty();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// AudioReviewPage
|
||||
// ===========================================================================
|
||||
|
||||
AudioReviewPage::AudioReviewPage(QWidget *parent, std::shared_ptr<Macro> ¯o)
|
||||
: QWizardPage(parent),
|
||||
_summary(new QLabel(this)),
|
||||
_macro(macro)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.audio.review.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.audio.review.subtitle"));
|
||||
|
||||
detail::setupSummaryLabel(_summary);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_summary);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void AudioReviewPage::initializePage()
|
||||
{
|
||||
const QString audioSource = field("audioSourceName").toString();
|
||||
const QString targetSource = field("audioTargetSource").toString();
|
||||
|
||||
auto *sourcePage = qobject_cast<AudioSourcePage *>(
|
||||
wizard()->page(PAGE_AUDIO_SOURCE));
|
||||
const double thresholdDb = sourcePage ? sourcePage->GetThresholdDb()
|
||||
: -24.0;
|
||||
const Duration duration = sourcePage ? sourcePage->GetDuration()
|
||||
: Duration(1.0);
|
||||
|
||||
_summary->setText(
|
||||
QString(obs_module_text("FirstRunWizard.audio.review.summary"))
|
||||
.arg(audioSource.toHtmlEscaped())
|
||||
.arg(thresholdDb, 0, 'f', 1)
|
||||
.arg(QString::fromStdString(duration.ToString()))
|
||||
.arg(targetSource.toHtmlEscaped()));
|
||||
}
|
||||
|
||||
bool AudioReviewPage::validatePage()
|
||||
{
|
||||
const QString audioSource = field("audioSourceName").toString();
|
||||
const QString targetSource = field("audioTargetSource").toString();
|
||||
const std::string name =
|
||||
("Audio: " + audioSource + " -> " + targetSource).toStdString();
|
||||
|
||||
auto *sourcePage = qobject_cast<AudioSourcePage *>(
|
||||
wizard()->page(PAGE_AUDIO_SOURCE));
|
||||
const double thresholdDb = sourcePage ? sourcePage->GetThresholdDb()
|
||||
: -24.0;
|
||||
const Duration duration = sourcePage ? sourcePage->GetDuration()
|
||||
: Duration(1.0);
|
||||
|
||||
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
|
||||
if (!_macro) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: audio macro allocation failed");
|
||||
return true;
|
||||
}
|
||||
|
||||
_macro->SetActionConditionSplitterPosition(
|
||||
{QWIDGETSIZE_MAX / 2, QWIDGETSIZE_MAX / 2});
|
||||
_macro->SetElseActionSplitterPosition(
|
||||
{QWIDGETSIZE_MAX / 2, QWIDGETSIZE_MAX / 2});
|
||||
|
||||
OBSDataAutoRelease condData =
|
||||
buildAudioConditionData(audioSource, thresholdDb, duration);
|
||||
if (!detail::addCondition(_macro.get(), "audio", condData)) {
|
||||
_macro.reset();
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"FirstRunWizard.audio.review.errorTitle"),
|
||||
obs_module_text(
|
||||
"FirstRunWizard.audio.review.errorBody"));
|
||||
return true;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease showData =
|
||||
buildVisibilityActionData(targetSource, 0);
|
||||
if (!detail::addAction(_macro.get(), "scene_visibility", showData)) {
|
||||
_macro.reset();
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"FirstRunWizard.audio.review.errorTitle"),
|
||||
obs_module_text(
|
||||
"FirstRunWizard.audio.review.errorBody"));
|
||||
return true;
|
||||
}
|
||||
|
||||
OBSDataAutoRelease hideData =
|
||||
buildVisibilityActionData(targetSource, 1);
|
||||
if (!detail::addElseAction(_macro.get(), "scene_visibility",
|
||||
hideData)) {
|
||||
_macro.reset();
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"FirstRunWizard.audio.review.errorTitle"),
|
||||
obs_module_text(
|
||||
"FirstRunWizard.audio.review.errorBody"));
|
||||
return true;
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "FirstRunWizard: created audio macro '%s'",
|
||||
name.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace wiz
|
||||
|
||||
} // namespace advss
|
||||
83
lib/utils/first-run-wizard-audio.hpp
Normal file
83
lib/utils/first-run-wizard-audio.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include "duration-control.hpp"
|
||||
#include "duration.hpp"
|
||||
#include "first-run-wizard.hpp"
|
||||
#include "volume-control.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace advss {
|
||||
|
||||
namespace wiz {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioSourcePage
|
||||
// Registers wizard field "audioSourceName" (QString).
|
||||
// Volume threshold and duration are read back via getters.
|
||||
// ---------------------------------------------------------------------------
|
||||
class AudioSourcePage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AudioSourcePage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_AUDIO_TARGET; }
|
||||
|
||||
double GetThresholdDb() const { return _thresholdSpinbox->value(); }
|
||||
Duration GetDuration() const;
|
||||
|
||||
private slots:
|
||||
void UpdateVolmeter();
|
||||
void SyncSpinboxFromSlider();
|
||||
void SyncSliderFromSpinbox();
|
||||
|
||||
private:
|
||||
QComboBox *_sourceCombo;
|
||||
QDoubleSpinBox *_thresholdSpinbox;
|
||||
DurationSelection *_durationSelection;
|
||||
QVBoxLayout *_volmeterLayout;
|
||||
VolControl *_volControl = nullptr;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioTargetPage
|
||||
// Registers wizard field "audioTargetSource" (QString).
|
||||
// ---------------------------------------------------------------------------
|
||||
class AudioTargetPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AudioTargetPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_AUDIO_REVIEW; }
|
||||
|
||||
private:
|
||||
QComboBox *_sourceCombo;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AudioReviewPage
|
||||
// Displays a summary and builds the macro from wizard fields on Finish.
|
||||
// ---------------------------------------------------------------------------
|
||||
class AudioReviewPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AudioReviewPage(QWidget *parent,
|
||||
std::shared_ptr<advss::Macro> ¯o);
|
||||
void initializePage() override;
|
||||
bool validatePage() override;
|
||||
int nextId() const override { return PAGE_DONE; }
|
||||
|
||||
private:
|
||||
QLabel *_summary;
|
||||
std::shared_ptr<advss::Macro> &_macro;
|
||||
};
|
||||
|
||||
} // namespace wiz
|
||||
} // namespace advss
|
||||
85
lib/utils/first-run-wizard-helpers.hpp
Normal file
85
lib/utils/first-run-wizard-helpers.hpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro-condition-factory.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace advss::wiz::detail {
|
||||
|
||||
static bool addCondition(advss::Macro *macro, const std::string &id,
|
||||
obs_data_t *data)
|
||||
{
|
||||
auto cond = MacroConditionFactory::Create(id, macro);
|
||||
if (!cond) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: condition factory returned null for '%s'",
|
||||
id.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!cond->Load(data)) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: condition Load() failed for '%s'",
|
||||
id.c_str());
|
||||
return false;
|
||||
}
|
||||
macro->Conditions().emplace_back(cond);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool addAction(advss::Macro *macro, const std::string &id,
|
||||
obs_data_t *data)
|
||||
{
|
||||
auto action = MacroActionFactory::Create(id, macro);
|
||||
if (!action) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: action factory returned null for '%s'",
|
||||
id.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!action->Load(data)) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: action Load() failed for '%s'",
|
||||
id.c_str());
|
||||
return false;
|
||||
}
|
||||
macro->Actions().emplace_back(action);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool addElseAction(advss::Macro *macro, const std::string &id,
|
||||
obs_data_t *data)
|
||||
{
|
||||
auto action = MacroActionFactory::Create(id, macro);
|
||||
if (!action) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: else-action factory returned null for '%s'",
|
||||
id.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!action->Load(data)) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: else-action Load() failed for '%s'",
|
||||
id.c_str());
|
||||
return false;
|
||||
}
|
||||
macro->ElseActions().emplace_back(action);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void setupSummaryLabel(QLabel *label)
|
||||
{
|
||||
label->setWordWrap(true);
|
||||
label->setTextFormat(Qt::RichText);
|
||||
label->setFrameShape(QFrame::StyledPanel);
|
||||
label->setContentsMargins(12, 12, 12, 12);
|
||||
}
|
||||
|
||||
} // namespace advss::wiz::detail
|
||||
499
lib/utils/first-run-wizard-sequence.cpp
Normal file
499
lib/utils/first-run-wizard-sequence.cpp
Normal file
@@ -0,0 +1,499 @@
|
||||
#include "first-run-wizard-sequence.hpp"
|
||||
#include "first-run-wizard-helpers.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
namespace wiz {
|
||||
|
||||
// Builds the obs_data blob for a "current scene == sceneName for at least
|
||||
// triggerDuration" condition, matching MacroConditionScene::Save().
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 2 },
|
||||
// "id": "scene",
|
||||
// "logic": 0,
|
||||
// "durationModifier": {
|
||||
// "time_constraint": 1, // AT_LEAST
|
||||
// "seconds": <Duration::Save output>
|
||||
// },
|
||||
// "sceneSelection": { "type": 0, "name": "<scene>", "canvasSelection": "Main" },
|
||||
// "type": 10, // CURRENT_SCENE
|
||||
// "version": 1
|
||||
// }
|
||||
static OBSDataAutoRelease
|
||||
buildSceneConditionData(const QString &scene, const Duration &triggerDuration)
|
||||
{
|
||||
OBSDataAutoRelease seg = obs_data_create();
|
||||
obs_data_set_bool(seg, "enabled", true);
|
||||
obs_data_set_int(seg, "version", 2);
|
||||
|
||||
OBSDataAutoRelease durMod = obs_data_create();
|
||||
obs_data_set_int(durMod, "time_constraint", 1);
|
||||
triggerDuration.Save(durMod, "seconds");
|
||||
|
||||
OBSDataAutoRelease sceneSel = obs_data_create();
|
||||
obs_data_set_int(sceneSel, "type", 0);
|
||||
obs_data_set_string(sceneSel, "name", scene.toUtf8().constData());
|
||||
obs_data_set_string(sceneSel, "canvasSelection", "Main");
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_obj(data, "segmentSettings", seg);
|
||||
obs_data_set_string(data, "id", "scene");
|
||||
obs_data_set_int(data, "logic", 0);
|
||||
obs_data_set_obj(data, "durationModifier", durMod);
|
||||
obs_data_set_obj(data, "sceneSelection", sceneSel);
|
||||
obs_data_set_int(data, "type", 10);
|
||||
obs_data_set_int(data, "version", 1);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Builds the obs_data blob for a scene-switch action,
|
||||
// matching MacroActionSwitchScene::Save().
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 2 },
|
||||
// "id": "scene_switch",
|
||||
// "action": 0,
|
||||
// "sceneSelection": { "type": 0, "name": "<scene>", "canvasSelection": "Main" },
|
||||
// "transitionType": 1, // scene's default transition
|
||||
// "blockUntilTransitionDone": true,
|
||||
// "sceneType": 0
|
||||
// }
|
||||
static OBSDataAutoRelease buildSceneSwitchData(const QString &scene)
|
||||
{
|
||||
OBSDataAutoRelease seg = obs_data_create();
|
||||
obs_data_set_bool(seg, "enabled", true);
|
||||
obs_data_set_int(seg, "version", 2);
|
||||
|
||||
OBSDataAutoRelease sceneSel = obs_data_create();
|
||||
obs_data_set_int(sceneSel, "type", 0);
|
||||
obs_data_set_string(sceneSel, "name", scene.toUtf8().constData());
|
||||
obs_data_set_string(sceneSel, "canvasSelection", "Main");
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_obj(data, "segmentSettings", seg);
|
||||
obs_data_set_string(data, "id", "scene_switch");
|
||||
obs_data_set_int(data, "action", 0);
|
||||
obs_data_set_obj(data, "sceneSelection", sceneSel);
|
||||
obs_data_set_int(data, "transitionType", 1);
|
||||
obs_data_set_bool(data, "blockUntilTransitionDone", true);
|
||||
obs_data_set_int(data, "sceneType", 0);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Builds the obs_data blob for a wait action, matching MacroActionWait::Save().
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 2 },
|
||||
// "id": "wait",
|
||||
// "duration": <Duration::Save output>,
|
||||
// "waitType": 0,
|
||||
// "version": 1
|
||||
// }
|
||||
static OBSDataAutoRelease buildWaitData(const Duration &duration)
|
||||
{
|
||||
OBSDataAutoRelease seg = obs_data_create();
|
||||
obs_data_set_bool(seg, "enabled", true);
|
||||
obs_data_set_int(seg, "version", 2);
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_obj(data, "segmentSettings", seg);
|
||||
obs_data_set_string(data, "id", "wait");
|
||||
duration.Save(data, "duration");
|
||||
obs_data_set_int(data, "waitType", 0);
|
||||
obs_data_set_int(data, "version", 1);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SequenceTriggerPage
|
||||
// ===========================================================================
|
||||
|
||||
SequenceTriggerPage::SequenceTriggerPage(QWidget *parent)
|
||||
: QWizardPage(parent),
|
||||
_sceneCombo(new QComboBox(this)),
|
||||
_delaySelection(new DurationSelection(this, true, 0.0))
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.seqTrigger.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.seqTrigger.subtitle"));
|
||||
|
||||
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
_delaySelection->SetDuration(Duration(5.0));
|
||||
|
||||
registerField("seqTriggerScene*", _sceneCombo, "currentText",
|
||||
SIGNAL(currentTextChanged(QString)));
|
||||
|
||||
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
|
||||
auto *sceneRow = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("FirstRunWizard.seqTrigger.scene"),
|
||||
sceneRow, {{"{{scene}}", _sceneCombo}}, false);
|
||||
|
||||
auto *delayRow = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("FirstRunWizard.seqTrigger.delay"),
|
||||
delayRow, {{"{{duration}}", _delaySelection}}, false);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(sceneRow);
|
||||
layout->addLayout(delayRow);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void SequenceTriggerPage::initializePage()
|
||||
{
|
||||
_sceneCombo->clear();
|
||||
for (const QString &name : GetSceneNames()) {
|
||||
_sceneCombo->addItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
bool SequenceTriggerPage::isComplete() const
|
||||
{
|
||||
return _sceneCombo->count() > 0 &&
|
||||
!_sceneCombo->currentText().isEmpty();
|
||||
}
|
||||
|
||||
Duration SequenceTriggerPage::GetTriggerDuration() const
|
||||
{
|
||||
return _delaySelection->GetDuration();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SequenceScenesPage
|
||||
// ===========================================================================
|
||||
|
||||
SequenceScenesPage::SequenceScenesPage(QWidget *parent)
|
||||
: QWizardPage(parent),
|
||||
_triggerInfoLabel(new QLabel(this)),
|
||||
_stepsContainer(new QWidget(this)),
|
||||
_stepsLayout(new QVBoxLayout(_stepsContainer))
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.seqScenes.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.seqScenes.subtitle"));
|
||||
|
||||
_triggerInfoLabel->setWordWrap(true);
|
||||
_triggerInfoLabel->setFrameShape(QFrame::StyledPanel);
|
||||
_triggerInfoLabel->setContentsMargins(8, 4, 8, 4);
|
||||
|
||||
_stepsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
_stepsLayout->setSpacing(4);
|
||||
|
||||
auto *scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidget(_stepsContainer);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto *addBtn = new QPushButton(
|
||||
obs_module_text("FirstRunWizard.seqScenes.addScene"), this);
|
||||
connect(addBtn, &QPushButton::clicked, this,
|
||||
&SequenceScenesPage::onAddStepClicked);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_triggerInfoLabel);
|
||||
layout->addWidget(scrollArea, 1);
|
||||
layout->addWidget(addBtn, 0, Qt::AlignLeft);
|
||||
}
|
||||
|
||||
void SequenceScenesPage::initializePage()
|
||||
{
|
||||
const QString triggerScene = field("seqTriggerScene").toString();
|
||||
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
|
||||
wizard()->page(PAGE_SEQ_TRIGGER));
|
||||
const Duration triggerDuration =
|
||||
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
|
||||
_triggerInfoLabel->setText(
|
||||
QString(obs_module_text("FirstRunWizard.seqScenes.triggerInfo"))
|
||||
.arg(triggerScene)
|
||||
.arg(QString::fromStdString(
|
||||
triggerDuration.ToString())));
|
||||
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
|
||||
// Pre-select the scenes after the trigger scene so the user doesn't
|
||||
// have to start by deselecting it manually.
|
||||
const QStringList scenes = GetSceneNames();
|
||||
const int count = static_cast<int>(scenes.size());
|
||||
const int triggerIdx = scenes.indexOf(triggerScene);
|
||||
const int firstIdx = count > 0 ? (triggerIdx + 1) % count : 0;
|
||||
const int secondIdx = count > 0 ? (triggerIdx + 2) % count : 0;
|
||||
AddStep(firstIdx);
|
||||
AddStep(secondIdx);
|
||||
}
|
||||
|
||||
bool SequenceScenesPage::isComplete() const
|
||||
{
|
||||
return _rows.size() >= 2;
|
||||
}
|
||||
|
||||
QVector<QPair<QString, Duration>> SequenceScenesPage::GetSteps() const
|
||||
{
|
||||
QVector<QPair<QString, Duration>> steps;
|
||||
steps.reserve(_rows.size());
|
||||
for (int i = 0; i < _rows.size(); ++i) {
|
||||
const Duration delay = (i < _rows.size() - 1)
|
||||
? _rows[i].delay->GetDuration()
|
||||
: Duration();
|
||||
steps.append({_rows[i].scene->currentText(), delay});
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
void SequenceScenesPage::onAddStepClicked()
|
||||
{
|
||||
const QStringList scenes = GetSceneNames();
|
||||
const int count = static_cast<int>(scenes.size());
|
||||
int nextIdx = 0;
|
||||
if (!_rows.isEmpty() && count > 0) {
|
||||
const int lastIdx = _rows.last().scene->currentIndex();
|
||||
nextIdx = (lastIdx + 1) % count;
|
||||
}
|
||||
AddStep(nextIdx);
|
||||
emit completeChanged();
|
||||
}
|
||||
|
||||
void SequenceScenesPage::AddStep(int defaultSceneIndex)
|
||||
{
|
||||
auto *row = new QWidget(_stepsContainer);
|
||||
auto *rowLayout = new QHBoxLayout(row);
|
||||
rowLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto *sceneCombo = new QComboBox(row);
|
||||
sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
const QStringList scenes = GetSceneNames();
|
||||
for (const QString &name : scenes) {
|
||||
sceneCombo->addItem(name);
|
||||
}
|
||||
if (defaultSceneIndex >= 0 && defaultSceneIndex < scenes.size()) {
|
||||
sceneCombo->setCurrentIndex(defaultSceneIndex);
|
||||
}
|
||||
PlaceWidgets(obs_module_text("FirstRunWizard.seqScenes.switchTo"),
|
||||
rowLayout, {{"{{scene}}", sceneCombo}}, false);
|
||||
|
||||
auto *delayWidget = new QWidget(row);
|
||||
auto *delayLayout = new QHBoxLayout(delayWidget);
|
||||
delayLayout->setContentsMargins(0, 0, 0, 0);
|
||||
auto *durationSel = new DurationSelection(delayWidget, true, 0.1);
|
||||
durationSel->SetDuration(Duration(5.0));
|
||||
PlaceWidgets(obs_module_text("FirstRunWizard.seqScenes.thenWait"),
|
||||
delayLayout, {{"{{duration}}", durationSel}}, false);
|
||||
rowLayout->addWidget(delayWidget);
|
||||
|
||||
auto *removeBtn = new QPushButton(row);
|
||||
removeBtn->setProperty("themeID",
|
||||
QVariant(QString::fromUtf8("removeIconSmall")));
|
||||
removeBtn->setProperty("class",
|
||||
QVariant(QString::fromUtf8("icon-trash")));
|
||||
removeBtn->setToolTip(
|
||||
obs_module_text("FirstRunWizard.seqScenes.removeTooltip"));
|
||||
removeBtn->setMaximumSize(22, 22);
|
||||
rowLayout->addWidget(removeBtn);
|
||||
|
||||
_rows.append({row, sceneCombo, delayWidget, durationSel, removeBtn});
|
||||
_stepsLayout->addWidget(row);
|
||||
|
||||
UpdateDelayVisibility();
|
||||
UpdateRemoveButtons();
|
||||
|
||||
connect(removeBtn, &QPushButton::clicked, this,
|
||||
[this, row]() { RemoveStep(row); });
|
||||
}
|
||||
|
||||
void SequenceScenesPage::RemoveStep(QWidget *rowWidget)
|
||||
{
|
||||
int idx = -1;
|
||||
for (int i = 0; i < _rows.size(); ++i) {
|
||||
if (_rows[i].row == rowWidget) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_stepsLayout->removeWidget(_rows[idx].row);
|
||||
_rows[idx].row->deleteLater();
|
||||
_rows.removeAt(idx);
|
||||
|
||||
UpdateDelayVisibility();
|
||||
UpdateRemoveButtons();
|
||||
emit completeChanged();
|
||||
}
|
||||
|
||||
void SequenceScenesPage::UpdateDelayVisibility()
|
||||
{
|
||||
for (int i = 0; i < _rows.size(); ++i) {
|
||||
_rows[i].delayWidget->setVisible(i < _rows.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void SequenceScenesPage::UpdateRemoveButtons()
|
||||
{
|
||||
const bool canRemove = _rows.size() > 1;
|
||||
for (auto &step : _rows) {
|
||||
step.remove->setEnabled(canRemove);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SequenceReviewPage
|
||||
// ===========================================================================
|
||||
|
||||
SequenceReviewPage::SequenceReviewPage(QWidget *parent,
|
||||
std::shared_ptr<Macro> ¯o)
|
||||
: QWizardPage(parent),
|
||||
_summary(new QLabel(this)),
|
||||
_macro(macro)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.seqReview.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.seqReview.subtitle"));
|
||||
|
||||
detail::setupSummaryLabel(_summary);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_summary);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void SequenceReviewPage::initializePage()
|
||||
{
|
||||
const QString triggerScene = field("seqTriggerScene").toString();
|
||||
|
||||
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
|
||||
wizard()->page(PAGE_SEQ_TRIGGER));
|
||||
const Duration triggerDuration =
|
||||
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
|
||||
|
||||
auto *seqPage = qobject_cast<SequenceScenesPage *>(
|
||||
wizard()->page(PAGE_SEQ_SCENES));
|
||||
const auto steps = seqPage ? seqPage->GetSteps()
|
||||
: QVector<QPair<QString, Duration>>{};
|
||||
|
||||
QString html =
|
||||
QString("<p>%1</p>")
|
||||
.arg(QString(obs_module_text(
|
||||
"FirstRunWizard.seqReview.trigger"))
|
||||
.arg(triggerScene.toHtmlEscaped())
|
||||
.arg(QString::fromStdString(
|
||||
triggerDuration.ToString())));
|
||||
|
||||
html += "<ol>";
|
||||
for (int i = 0; i < steps.size(); ++i) {
|
||||
const QString scene = steps[i].first.toHtmlEscaped();
|
||||
if (i == 0) {
|
||||
html += "<li>" + scene + "</li>";
|
||||
} else {
|
||||
const QString waitStr = QString::fromStdString(
|
||||
steps[i - 1].second.ToString());
|
||||
html += "<li>" +
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.seqReview.step"))
|
||||
.arg(waitStr)
|
||||
.arg(scene) +
|
||||
"</li>";
|
||||
}
|
||||
}
|
||||
html += "</ol>";
|
||||
|
||||
_summary->setText(html);
|
||||
}
|
||||
|
||||
bool SequenceReviewPage::validatePage()
|
||||
{
|
||||
const QString triggerScene = field("seqTriggerScene").toString();
|
||||
const std::string name = ("Sequence: " + triggerScene).toStdString();
|
||||
|
||||
auto *triggerPage = qobject_cast<SequenceTriggerPage *>(
|
||||
wizard()->page(PAGE_SEQ_TRIGGER));
|
||||
const Duration triggerDuration =
|
||||
triggerPage ? triggerPage->GetTriggerDuration() : Duration(5.0);
|
||||
|
||||
auto *seqPage = qobject_cast<SequenceScenesPage *>(
|
||||
wizard()->page(PAGE_SEQ_SCENES));
|
||||
if (!seqPage) {
|
||||
return true;
|
||||
}
|
||||
const auto steps = seqPage->GetSteps();
|
||||
|
||||
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
|
||||
if (!_macro) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: sequence macro allocation failed");
|
||||
return true;
|
||||
}
|
||||
_macro->SetRunInParallel(true);
|
||||
|
||||
// --- Condition ---
|
||||
OBSDataAutoRelease condData =
|
||||
buildSceneConditionData(triggerScene, triggerDuration);
|
||||
if (!detail::addCondition(_macro.get(), "scene", condData)) {
|
||||
_macro.reset();
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text("FirstRunWizard.seqReview.errorTitle"),
|
||||
obs_module_text("FirstRunWizard.seqReview.errorBody"));
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Actions: scene_switch interleaved with wait ---
|
||||
for (int i = 0; i < steps.size(); ++i) {
|
||||
OBSDataAutoRelease switchData =
|
||||
buildSceneSwitchData(steps[i].first);
|
||||
if (!detail::addAction(_macro.get(), "scene_switch",
|
||||
switchData)) {
|
||||
_macro.reset();
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"FirstRunWizard.seqReview.errorTitle"),
|
||||
obs_module_text(
|
||||
"FirstRunWizard.seqReview.errorBody"));
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool isLastStep = (i == steps.size() - 1);
|
||||
if (!isLastStep) {
|
||||
OBSDataAutoRelease waitData =
|
||||
buildWaitData(steps[i].second);
|
||||
if (!detail::addAction(_macro.get(), "wait",
|
||||
waitData)) {
|
||||
_macro.reset();
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text(
|
||||
"FirstRunWizard.seqReview.errorTitle"),
|
||||
obs_module_text(
|
||||
"FirstRunWizard.seqReview.errorBody"));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "FirstRunWizard: created sequence macro '%s'",
|
||||
name.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace wiz
|
||||
|
||||
} // namespace advss
|
||||
101
lib/utils/first-run-wizard-sequence.hpp
Normal file
101
lib/utils/first-run-wizard-sequence.hpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include "duration-control.hpp"
|
||||
#include "duration.hpp"
|
||||
#include "first-run-wizard.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVector>
|
||||
#include <QWidget>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QVBoxLayout;
|
||||
|
||||
namespace advss {
|
||||
|
||||
namespace wiz {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SequenceTriggerPage
|
||||
// Registers wizard field "seqTriggerScene" (QString).
|
||||
// Trigger duration is read back via GetTriggerDuration().
|
||||
// ---------------------------------------------------------------------------
|
||||
class SequenceTriggerPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SequenceTriggerPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_SEQ_SCENES; }
|
||||
|
||||
Duration GetTriggerDuration() const;
|
||||
|
||||
private:
|
||||
QComboBox *_sceneCombo;
|
||||
DurationSelection *_delaySelection;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SequenceScenesPage
|
||||
// Lets the user build an ordered list of scenes with delays between them.
|
||||
// The delay after the last scene is ignored when building the macro.
|
||||
// ---------------------------------------------------------------------------
|
||||
struct SequenceStep {
|
||||
QWidget *row;
|
||||
QComboBox *scene;
|
||||
QWidget *delayWidget; // hidden for the last row
|
||||
DurationSelection *delay;
|
||||
QPushButton *remove;
|
||||
};
|
||||
|
||||
class SequenceScenesPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SequenceScenesPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_SEQ_REVIEW; }
|
||||
|
||||
// Returns (sceneName, delayAfter) pairs. The last entry's delayAfter
|
||||
// is always a default-constructed Duration and must be ignored by the caller.
|
||||
QVector<QPair<QString, Duration>> GetSteps() const;
|
||||
|
||||
private slots:
|
||||
void onAddStepClicked();
|
||||
|
||||
private:
|
||||
void AddStep(int defaultSceneIndex = 0);
|
||||
void RemoveStep(QWidget *rowWidget);
|
||||
void UpdateDelayVisibility();
|
||||
void UpdateRemoveButtons();
|
||||
|
||||
QLabel *_triggerInfoLabel;
|
||||
QWidget *_stepsContainer;
|
||||
QVBoxLayout *_stepsLayout;
|
||||
QVector<SequenceStep> _rows;
|
||||
bool _initialized = false;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SequenceReviewPage
|
||||
// Displays a summary and builds the macro from wizard fields on Finish.
|
||||
// ---------------------------------------------------------------------------
|
||||
class SequenceReviewPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SequenceReviewPage(QWidget *parent,
|
||||
std::shared_ptr<advss::Macro> ¯o);
|
||||
void initializePage() override;
|
||||
bool validatePage() override;
|
||||
int nextId() const override { return PAGE_DONE; }
|
||||
|
||||
private:
|
||||
QLabel *_summary;
|
||||
std::shared_ptr<advss::Macro> &_macro;
|
||||
};
|
||||
|
||||
} // namespace wiz
|
||||
} // namespace advss
|
||||
308
lib/utils/first-run-wizard-window.cpp
Normal file
308
lib/utils/first-run-wizard-window.cpp
Normal file
@@ -0,0 +1,308 @@
|
||||
#include "first-run-wizard-window.hpp"
|
||||
#include "first-run-wizard-helpers.hpp"
|
||||
|
||||
#include "macro-settings.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "platform-funcs.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QRegularExpression>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
namespace wiz {
|
||||
|
||||
static constexpr char kConditionIdWindow[] = "window";
|
||||
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
|
||||
|
||||
static QString detectFocusedWindow()
|
||||
{
|
||||
return QString::fromStdString(GetCurrentWindowTitle());
|
||||
}
|
||||
|
||||
static QString escapeForRegex(const QString &input)
|
||||
{
|
||||
return QRegularExpression::escape(input);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WindowSceneSelectionPage
|
||||
// ===========================================================================
|
||||
|
||||
WindowSceneSelectionPage::WindowSceneSelectionPage(QWidget *parent)
|
||||
: QWizardPage(parent)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.scene.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.scene.subtitle"));
|
||||
|
||||
auto label =
|
||||
new QLabel(obs_module_text("FirstRunWizard.scene.label"), this);
|
||||
_sceneCombo = new QComboBox(this);
|
||||
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
// registerField with * suffix means the field is mandatory for Next
|
||||
registerField("targetScene*", _sceneCombo, "currentText",
|
||||
SIGNAL(currentTextChanged(QString)));
|
||||
|
||||
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
|
||||
auto row = new QHBoxLayout;
|
||||
row->addWidget(label);
|
||||
row->addWidget(_sceneCombo, 1);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addLayout(row);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void WindowSceneSelectionPage::initializePage()
|
||||
{
|
||||
_sceneCombo->clear();
|
||||
for (const QString &name : GetSceneNames()) {
|
||||
_sceneCombo->addItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowSceneSelectionPage::isComplete() const
|
||||
{
|
||||
return _sceneCombo->count() > 0 &&
|
||||
!_sceneCombo->currentText().isEmpty();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WindowConditionPage
|
||||
// ===========================================================================
|
||||
|
||||
WindowConditionPage::WindowConditionPage(QWidget *parent)
|
||||
: QWizardPage(parent),
|
||||
_detectTimer(new QTimer(this))
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.window.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.window.subtitle"));
|
||||
|
||||
auto label = new QLabel(obs_module_text("FirstRunWizard.window.label"),
|
||||
this);
|
||||
_windowEdit = new QLineEdit(this);
|
||||
_windowEdit->setPlaceholderText(
|
||||
obs_module_text("FirstRunWizard.window.placeholder"));
|
||||
|
||||
_autoDetect = new QPushButton(
|
||||
obs_module_text("FirstRunWizard.window.autoDetect"), this);
|
||||
_autoDetect->setToolTip(
|
||||
obs_module_text("FirstRunWizard.window.autoDetectTooltip"));
|
||||
|
||||
registerField("windowTitle*", _windowEdit);
|
||||
|
||||
connect(_windowEdit, &QLineEdit::textChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
connect(_autoDetect, &QPushButton::clicked, this,
|
||||
&WindowConditionPage::onAutoDetectClicked);
|
||||
connect(_detectTimer, &QTimer::timeout, this,
|
||||
&WindowConditionPage::onCountdownTick);
|
||||
|
||||
auto row = new QHBoxLayout;
|
||||
row->addWidget(label);
|
||||
row->addWidget(_windowEdit, 1);
|
||||
|
||||
auto hint =
|
||||
new QLabel(obs_module_text("FirstRunWizard.window.hint"), this);
|
||||
hint->setTextFormat(Qt::RichText);
|
||||
hint->setWordWrap(true);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addLayout(row);
|
||||
layout->addWidget(_autoDetect, 0, Qt::AlignLeft);
|
||||
layout->addWidget(hint);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void WindowConditionPage::initializePage()
|
||||
{
|
||||
if (_windowEdit->text().isEmpty()) {
|
||||
QString detected = detectFocusedWindow();
|
||||
if (!detected.isEmpty()) {
|
||||
_windowEdit->setText(detected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowConditionPage::isComplete() const
|
||||
{
|
||||
return !_windowEdit->text().trimmed().isEmpty();
|
||||
}
|
||||
|
||||
void WindowConditionPage::onAutoDetectClicked()
|
||||
{
|
||||
_countdown = 3;
|
||||
_autoDetect->setEnabled(false);
|
||||
_autoDetect->setText(
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.window.autoDetectCountdown"))
|
||||
.arg(_countdown));
|
||||
_detectTimer->start(1000);
|
||||
}
|
||||
|
||||
void WindowConditionPage::onCountdownTick()
|
||||
{
|
||||
--_countdown;
|
||||
if (_countdown > 0) {
|
||||
_autoDetect->setText(
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.window.autoDetectCountdown"))
|
||||
.arg(_countdown));
|
||||
return;
|
||||
}
|
||||
|
||||
_detectTimer->stop();
|
||||
QString title = detectFocusedWindow();
|
||||
if (!title.isEmpty()) {
|
||||
_windowEdit->setText(title);
|
||||
}
|
||||
|
||||
_autoDetect->setEnabled(true);
|
||||
_autoDetect->setText(
|
||||
obs_module_text("FirstRunWizard.window.autoDetect"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WindowReviewPage
|
||||
// ===========================================================================
|
||||
|
||||
WindowReviewPage::WindowReviewPage(QWidget *parent,
|
||||
std::shared_ptr<Macro> ¯o)
|
||||
: QWizardPage(parent),
|
||||
_macro(macro)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.review.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.review.subtitle"));
|
||||
|
||||
_summary = new QLabel(this);
|
||||
detail::setupSummaryLabel(_summary);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_summary);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void WindowReviewPage::initializePage()
|
||||
{
|
||||
const QString scene = field("targetScene").toString();
|
||||
const QString window = field("windowTitle").toString();
|
||||
|
||||
_summary->setText(
|
||||
QString(obs_module_text("FirstRunWizard.review.summary"))
|
||||
.arg(scene.toHtmlEscaped(), window.toHtmlEscaped()));
|
||||
}
|
||||
|
||||
bool WindowReviewPage::validatePage()
|
||||
{
|
||||
const QString scene = field("targetScene").toString();
|
||||
const QString window = escapeForRegex(field("windowTitle").toString());
|
||||
const std::string name = ("Window -> " + scene).toStdString();
|
||||
|
||||
// Build condition data blob
|
||||
// ---------------------------------------------------------------
|
||||
// Condition blob — mirrors MacroConditionWindow::Save() output:
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 1 },
|
||||
// "id": "window",
|
||||
// "checkTitle": true,
|
||||
// "window": "<user input>",
|
||||
// "windowRegexConfig": {
|
||||
// "enable": true, // use regex-style partial matching
|
||||
// "partial": true, // match anywhere in the title
|
||||
// "options": 3 // case-insensitive (QRegularExpression flags)
|
||||
// },
|
||||
// "focus": true, // only trigger when window is focused
|
||||
// "version": 1
|
||||
// }
|
||||
// ---------------------------------------------------------------
|
||||
OBSDataAutoRelease condSegment = obs_data_create();
|
||||
obs_data_set_bool(condSegment, "enabled", true);
|
||||
obs_data_set_int(condSegment, "version", 1);
|
||||
|
||||
OBSDataAutoRelease condRegex = obs_data_create();
|
||||
obs_data_set_bool(condRegex, "enable", true);
|
||||
obs_data_set_bool(condRegex, "partial", true);
|
||||
obs_data_set_int(condRegex, "options", 3); // CaseInsensitiveOption
|
||||
|
||||
OBSDataAutoRelease condData = obs_data_create();
|
||||
obs_data_set_obj(condData, "segmentSettings", condSegment);
|
||||
obs_data_set_string(condData, "id", "window");
|
||||
obs_data_set_bool(condData, "checkTitle", true);
|
||||
obs_data_set_string(condData, "window", window.toUtf8().constData());
|
||||
obs_data_set_obj(condData, "windowRegexConfig", condRegex);
|
||||
obs_data_set_bool(condData, "focus", true);
|
||||
obs_data_set_int(condData, "version", 1);
|
||||
|
||||
// Build action data blob
|
||||
// ---------------------------------------------------------------
|
||||
// Action blob — mirrors MacroActionSwitchScene::Save() output:
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 1 },
|
||||
// "id": "scene_switch",
|
||||
// "action": 0, // 0 = switch scene
|
||||
// "sceneSelection": {
|
||||
// "type": 0, // 0 = scene by name
|
||||
// "name": "<scene>",
|
||||
// "canvasSelection": "Main"
|
||||
// },
|
||||
// "transitionType": 1, // 1 = use scene's default transition
|
||||
// "blockUntilTransitionDone": false,
|
||||
// "sceneType": 0
|
||||
// }
|
||||
// ---------------------------------------------------------------
|
||||
OBSDataAutoRelease actionSegment = obs_data_create();
|
||||
obs_data_set_bool(actionSegment, "enabled", true);
|
||||
obs_data_set_int(actionSegment, "version", 1);
|
||||
|
||||
OBSDataAutoRelease sceneSelection = obs_data_create();
|
||||
obs_data_set_int(sceneSelection, "type", 0);
|
||||
obs_data_set_string(sceneSelection, "name", scene.toUtf8().constData());
|
||||
obs_data_set_string(sceneSelection, "canvasSelection", "Main");
|
||||
|
||||
OBSDataAutoRelease actionData = obs_data_create();
|
||||
obs_data_set_obj(actionData, "segmentSettings", actionSegment);
|
||||
obs_data_set_string(actionData, "id", "scene_switch");
|
||||
obs_data_set_int(actionData, "action", 0);
|
||||
obs_data_set_obj(actionData, "sceneSelection", sceneSelection);
|
||||
obs_data_set_int(actionData, "transitionType", 1);
|
||||
obs_data_set_bool(actionData, "blockUntilTransitionDone", false);
|
||||
obs_data_set_int(actionData, "sceneType", 0);
|
||||
|
||||
_macro = std::make_shared<Macro>(name, GetGlobalMacroSettings());
|
||||
if (!_macro) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: window macro allocation failed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!detail::addCondition(_macro.get(), kConditionIdWindow, condData) ||
|
||||
!detail::addAction(_macro.get(), kActionIdSceneSwitch,
|
||||
actionData)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text("FirstRunWizard.review.errorTitle"),
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.review.errorBody"))
|
||||
.arg(window, scene));
|
||||
_macro.reset();
|
||||
// Still advance so the user is not stuck.
|
||||
return true;
|
||||
}
|
||||
|
||||
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", name.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace wiz
|
||||
|
||||
} // namespace advss
|
||||
76
lib/utils/first-run-wizard-window.hpp
Normal file
76
lib/utils/first-run-wizard-window.hpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "first-run-wizard.hpp"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace advss {
|
||||
|
||||
namespace wiz {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WindowSceneSelectionPage
|
||||
// Registers wizard field "targetScene" (QString).
|
||||
// ---------------------------------------------------------------------------
|
||||
class WindowSceneSelectionPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WindowSceneSelectionPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_WINDOW_CONDITION; }
|
||||
|
||||
private:
|
||||
QComboBox *_sceneCombo;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WindowConditionPage
|
||||
// Registers wizard field "windowTitle" (QString).
|
||||
// Auto-detect button samples the focused window after a countdown.
|
||||
// ---------------------------------------------------------------------------
|
||||
class WindowConditionPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WindowConditionPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_WINDOW_REVIEW; }
|
||||
|
||||
private slots:
|
||||
void onAutoDetectClicked();
|
||||
void onCountdownTick();
|
||||
|
||||
private:
|
||||
QLineEdit *_windowEdit;
|
||||
QPushButton *_autoDetect;
|
||||
QTimer *_detectTimer;
|
||||
int _countdown = 3;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WindowReviewPage
|
||||
// Displays a summary and builds the macro from wizard fields on Finish.
|
||||
// ---------------------------------------------------------------------------
|
||||
class WindowReviewPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WindowReviewPage(QWidget *parent,
|
||||
std::shared_ptr<advss::Macro> ¯o);
|
||||
void initializePage() override;
|
||||
bool validatePage() override;
|
||||
int nextId() const override { return PAGE_DONE; }
|
||||
|
||||
private:
|
||||
QLabel *_summary;
|
||||
std::shared_ptr<advss::Macro> &_macro;
|
||||
};
|
||||
|
||||
} // namespace wiz
|
||||
} // namespace advss
|
||||
@@ -1,28 +1,17 @@
|
||||
#include "first-run-wizard.hpp"
|
||||
#include "first-run-wizard-audio.hpp"
|
||||
#include "first-run-wizard-sequence.hpp"
|
||||
#include "first-run-wizard-window.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro-condition-factory.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "platform-funcs.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs-data.h>
|
||||
#include <util/config-file.h>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QRegularExpression>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static constexpr char kConditionIdWindow[] = "window";
|
||||
static constexpr char kActionIdSceneSwitch[] = "scene_switch";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OBS global config helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -42,7 +31,7 @@ bool IsFirstRun()
|
||||
return config_get_bool(cfg, kConfigSection, kFirstRunKey);
|
||||
}
|
||||
|
||||
static void WriteFirstRun(bool value)
|
||||
static void writeFirstRun(bool value)
|
||||
{
|
||||
#if LIBOBS_API_VER >= MAKE_SEMANTIC_VERSION(31, 0, 0)
|
||||
config_t *cfg = obs_frontend_get_user_config();
|
||||
@@ -53,10 +42,10 @@ static void WriteFirstRun(bool value)
|
||||
config_save_safe(cfg, "tmp", nullptr);
|
||||
}
|
||||
|
||||
static QString DetectFocusedWindow()
|
||||
{
|
||||
return QString::fromStdString(GetCurrentWindowTitle());
|
||||
}
|
||||
} // namespace advss
|
||||
|
||||
namespace advss {
|
||||
namespace wiz {
|
||||
|
||||
// ===========================================================================
|
||||
// WelcomePage
|
||||
@@ -78,271 +67,39 @@ WelcomePage::WelcomePage(QWidget *parent) : QWizardPage(parent)
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// SceneSelectionPage
|
||||
// TemplatePage
|
||||
// ===========================================================================
|
||||
|
||||
SceneSelectionPage::SceneSelectionPage(QWidget *parent) : QWizardPage(parent)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.scene.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.scene.subtitle"));
|
||||
|
||||
auto label =
|
||||
new QLabel(obs_module_text("FirstRunWizard.scene.label"), this);
|
||||
_sceneCombo = new QComboBox(this);
|
||||
_sceneCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
|
||||
// registerField with * suffix means the field is mandatory for Next
|
||||
registerField("targetScene*", _sceneCombo, "currentText",
|
||||
SIGNAL(currentTextChanged(QString)));
|
||||
|
||||
connect(_sceneCombo, &QComboBox::currentTextChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
|
||||
auto row = new QHBoxLayout;
|
||||
row->addWidget(label);
|
||||
row->addWidget(_sceneCombo, 1);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addLayout(row);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void SceneSelectionPage::initializePage()
|
||||
{
|
||||
_sceneCombo->clear();
|
||||
for (const QString &name : GetSceneNames())
|
||||
_sceneCombo->addItem(name);
|
||||
}
|
||||
|
||||
bool SceneSelectionPage::isComplete() const
|
||||
{
|
||||
return _sceneCombo->count() > 0 &&
|
||||
!_sceneCombo->currentText().isEmpty();
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// WindowConditionPage
|
||||
// ===========================================================================
|
||||
|
||||
WindowConditionPage::WindowConditionPage(QWidget *parent)
|
||||
TemplatePage::TemplatePage(QWidget *parent)
|
||||
: QWizardPage(parent),
|
||||
_detectTimer(new QTimer(this))
|
||||
_windowRadio(new QRadioButton(
|
||||
obs_module_text("FirstRunWizard.template.window"), this)),
|
||||
_sequenceRadio(new QRadioButton(
|
||||
obs_module_text("FirstRunWizard.template.sequence"), this)),
|
||||
_audioRadio(new QRadioButton(
|
||||
obs_module_text("FirstRunWizard.template.audio"), this))
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.window.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.window.subtitle"));
|
||||
setTitle(obs_module_text("FirstRunWizard.template.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.template.subtitle"));
|
||||
|
||||
auto label = new QLabel(obs_module_text("FirstRunWizard.window.label"),
|
||||
this);
|
||||
_windowEdit = new QLineEdit(this);
|
||||
_windowEdit->setPlaceholderText(
|
||||
obs_module_text("FirstRunWizard.window.placeholder"));
|
||||
_windowRadio->setChecked(true);
|
||||
|
||||
_autoDetect = new QPushButton(
|
||||
obs_module_text("FirstRunWizard.window.autoDetect"), this);
|
||||
_autoDetect->setToolTip(
|
||||
obs_module_text("FirstRunWizard.window.autoDetectTooltip"));
|
||||
|
||||
registerField("windowTitle*", _windowEdit);
|
||||
|
||||
connect(_windowEdit, &QLineEdit::textChanged, this,
|
||||
&QWizardPage::completeChanged);
|
||||
connect(_autoDetect, &QPushButton::clicked, this,
|
||||
&WindowConditionPage::onAutoDetectClicked);
|
||||
connect(_detectTimer, &QTimer::timeout, this,
|
||||
&WindowConditionPage::onCountdownTick);
|
||||
|
||||
auto row = new QHBoxLayout;
|
||||
row->addWidget(label);
|
||||
row->addWidget(_windowEdit, 1);
|
||||
|
||||
auto hint =
|
||||
new QLabel(obs_module_text("FirstRunWizard.window.hint"), this);
|
||||
hint->setTextFormat(Qt::RichText);
|
||||
hint->setWordWrap(true);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addLayout(row);
|
||||
layout->addWidget(_autoDetect, 0, Qt::AlignLeft);
|
||||
layout->addWidget(hint);
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_windowRadio);
|
||||
layout->addWidget(_sequenceRadio);
|
||||
layout->addWidget(_audioRadio);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void WindowConditionPage::initializePage()
|
||||
int TemplatePage::nextId() const
|
||||
{
|
||||
if (_windowEdit->text().isEmpty()) {
|
||||
QString detected = DetectFocusedWindow();
|
||||
if (!detected.isEmpty()) {
|
||||
_windowEdit->setText(detected);
|
||||
}
|
||||
if (_sequenceRadio->isChecked()) {
|
||||
return PAGE_SEQ_TRIGGER;
|
||||
}
|
||||
}
|
||||
|
||||
bool WindowConditionPage::isComplete() const
|
||||
{
|
||||
return !_windowEdit->text().trimmed().isEmpty();
|
||||
}
|
||||
|
||||
void WindowConditionPage::onAutoDetectClicked()
|
||||
{
|
||||
_countdown = 3;
|
||||
_autoDetect->setEnabled(false);
|
||||
_autoDetect->setText(
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.window.autoDetectCountdown"))
|
||||
.arg(_countdown));
|
||||
_detectTimer->start(1000);
|
||||
}
|
||||
|
||||
void WindowConditionPage::onCountdownTick()
|
||||
{
|
||||
--_countdown;
|
||||
if (_countdown > 0) {
|
||||
_autoDetect->setText(
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.window.autoDetectCountdown"))
|
||||
.arg(_countdown));
|
||||
return;
|
||||
if (_audioRadio->isChecked()) {
|
||||
return PAGE_AUDIO_SOURCE;
|
||||
}
|
||||
|
||||
_detectTimer->stop();
|
||||
QString title = DetectFocusedWindow();
|
||||
if (!title.isEmpty()) {
|
||||
_windowEdit->setText(title);
|
||||
}
|
||||
|
||||
_autoDetect->setEnabled(true);
|
||||
_autoDetect->setText(
|
||||
obs_module_text("FirstRunWizard.window.autoDetect"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// ReviewPage
|
||||
// ===========================================================================
|
||||
|
||||
ReviewPage::ReviewPage(QWidget *parent, std::shared_ptr<Macro> ¯o)
|
||||
: QWizardPage(parent),
|
||||
_macro(macro)
|
||||
{
|
||||
setTitle(obs_module_text("FirstRunWizard.review.title"));
|
||||
setSubTitle(obs_module_text("FirstRunWizard.review.subtitle"));
|
||||
|
||||
_summary = new QLabel(this);
|
||||
_summary->setWordWrap(true);
|
||||
_summary->setTextFormat(Qt::RichText);
|
||||
_summary->setFrameShape(QFrame::StyledPanel);
|
||||
_summary->setContentsMargins(12, 12, 12, 12);
|
||||
|
||||
auto layout = new QVBoxLayout(this);
|
||||
layout->addWidget(_summary);
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void ReviewPage::initializePage()
|
||||
{
|
||||
const QString scene = field("targetScene").toString();
|
||||
const QString window = field("windowTitle").toString();
|
||||
|
||||
_summary->setText(
|
||||
QString(obs_module_text("FirstRunWizard.review.summary"))
|
||||
.arg(scene.toHtmlEscaped(), window.toHtmlEscaped()));
|
||||
}
|
||||
|
||||
static QString escapeForRegex(const QString &input)
|
||||
{
|
||||
return QRegularExpression::escape(input);
|
||||
}
|
||||
|
||||
bool ReviewPage::validatePage()
|
||||
{
|
||||
const QString scene = field("targetScene").toString();
|
||||
const QString window = escapeForRegex(field("windowTitle").toString());
|
||||
const std::string name = ("Window -> " + scene).toStdString();
|
||||
|
||||
// Build condition data blob
|
||||
// ---------------------------------------------------------------
|
||||
// Condition blob — mirrors MacroConditionWindow::Save() output:
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 1 },
|
||||
// "id": "window",
|
||||
// "checkTitle": true,
|
||||
// "window": "<user input>",
|
||||
// "windowRegexConfig": {
|
||||
// "enable": true, // use regex-style partial matching
|
||||
// "partial": true, // match anywhere in the title
|
||||
// "options": 3 // case-insensitive (QRegularExpression flags)
|
||||
// },
|
||||
// "focus": true, // only trigger when window is focused
|
||||
// "version": 1
|
||||
// }
|
||||
// ---------------------------------------------------------------
|
||||
OBSDataAutoRelease condSegment = obs_data_create();
|
||||
obs_data_set_bool(condSegment, "enabled", true);
|
||||
obs_data_set_int(condSegment, "version", 1);
|
||||
|
||||
OBSDataAutoRelease condRegex = obs_data_create();
|
||||
obs_data_set_bool(condRegex, "enable", true);
|
||||
obs_data_set_bool(condRegex, "partial", true);
|
||||
obs_data_set_int(condRegex, "options", 3); // CaseInsensitiveOption
|
||||
|
||||
OBSDataAutoRelease condData = obs_data_create();
|
||||
obs_data_set_obj(condData, "segmentSettings", condSegment);
|
||||
obs_data_set_string(condData, "id", "window");
|
||||
obs_data_set_bool(condData, "checkTitle", true);
|
||||
obs_data_set_string(condData, "window", window.toUtf8().constData());
|
||||
obs_data_set_obj(condData, "windowRegexConfig", condRegex);
|
||||
obs_data_set_bool(condData, "focus", true);
|
||||
obs_data_set_int(condData, "version", 1);
|
||||
|
||||
// Build action data blob
|
||||
// ---------------------------------------------------------------
|
||||
// Action blob — mirrors MacroActionSwitchScene::Save() output:
|
||||
//
|
||||
// {
|
||||
// "segmentSettings": { "enabled": true, "version": 1 },
|
||||
// "id": "scene_switch",
|
||||
// "action": 0, // 0 = switch scene
|
||||
// "sceneSelection": {
|
||||
// "type": 0, // 0 = scene by name
|
||||
// "name": "<scene>",
|
||||
// "canvasSelection": "Main"
|
||||
// },
|
||||
// "transitionType": 1, // 1 = use scene's default transition
|
||||
// "blockUntilTransitionDone": false,
|
||||
// "sceneType": 0
|
||||
// }
|
||||
// ---------------------------------------------------------------
|
||||
OBSDataAutoRelease actionSegment = obs_data_create();
|
||||
obs_data_set_bool(actionSegment, "enabled", true);
|
||||
obs_data_set_int(actionSegment, "version", 1);
|
||||
|
||||
OBSDataAutoRelease sceneSelection = obs_data_create();
|
||||
obs_data_set_int(sceneSelection, "type", 0);
|
||||
obs_data_set_string(sceneSelection, "name", scene.toUtf8().constData());
|
||||
obs_data_set_string(sceneSelection, "canvasSelection", "Main");
|
||||
|
||||
OBSDataAutoRelease actionData = obs_data_create();
|
||||
obs_data_set_obj(actionData, "segmentSettings", actionSegment);
|
||||
obs_data_set_string(actionData, "id", "scene_switch");
|
||||
obs_data_set_int(actionData, "action", 0);
|
||||
obs_data_set_obj(actionData, "sceneSelection", sceneSelection);
|
||||
obs_data_set_int(actionData, "transitionType", 1);
|
||||
obs_data_set_bool(actionData, "blockUntilTransitionDone", false);
|
||||
obs_data_set_int(actionData, "sceneType", 0);
|
||||
|
||||
if (!FirstRunWizard::CreateMacro(_macro, name, kConditionIdWindow,
|
||||
condData, kActionIdSceneSwitch,
|
||||
actionData)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
obs_module_text("FirstRunWizard.review.errorTitle"),
|
||||
QString(obs_module_text(
|
||||
"FirstRunWizard.review.errorBody"))
|
||||
.arg(window, scene));
|
||||
_macro.reset();
|
||||
// Still advance so the user is not stuck.
|
||||
}
|
||||
return true;
|
||||
return PAGE_WINDOW_SCENE;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -373,12 +130,19 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent)
|
||||
{
|
||||
setWindowTitle(obs_module_text("FirstRunWizard.windowTitle"));
|
||||
setWizardStyle(QWizard::ModernStyle);
|
||||
setMinimumSize(540, 420);
|
||||
setMinimumSize(600, 420);
|
||||
|
||||
setPage(PAGE_WELCOME, new WelcomePage(this));
|
||||
setPage(PAGE_SCENE, new SceneSelectionPage(this));
|
||||
setPage(PAGE_WINDOW, new WindowConditionPage(this));
|
||||
setPage(PAGE_REVIEW, new ReviewPage(this, _macro));
|
||||
setPage(PAGE_TEMPLATE, new TemplatePage(this));
|
||||
setPage(PAGE_WINDOW_SCENE, new WindowSceneSelectionPage(this));
|
||||
setPage(PAGE_WINDOW_CONDITION, new WindowConditionPage(this));
|
||||
setPage(PAGE_WINDOW_REVIEW, new WindowReviewPage(this, _macro));
|
||||
setPage(PAGE_SEQ_TRIGGER, new SequenceTriggerPage(this));
|
||||
setPage(PAGE_SEQ_SCENES, new SequenceScenesPage(this));
|
||||
setPage(PAGE_SEQ_REVIEW, new SequenceReviewPage(this, _macro));
|
||||
setPage(PAGE_AUDIO_SOURCE, new AudioSourcePage(this));
|
||||
setPage(PAGE_AUDIO_TARGET, new AudioTargetPage(this));
|
||||
setPage(PAGE_AUDIO_REVIEW, new AudioReviewPage(this, _macro));
|
||||
setPage(PAGE_DONE, new DonePage(this));
|
||||
|
||||
setStartId(PAGE_WELCOME);
|
||||
@@ -394,7 +158,7 @@ FirstRunWizard::FirstRunWizard(QWidget *parent) : QWizard(parent)
|
||||
|
||||
void FirstRunWizard::markFirstRunComplete()
|
||||
{
|
||||
WriteFirstRun(false);
|
||||
writeFirstRun(false);
|
||||
}
|
||||
|
||||
// static
|
||||
@@ -410,58 +174,5 @@ std::shared_ptr<Macro> FirstRunWizard::ShowWizard(QWidget *parent,
|
||||
return wizard->_macro;
|
||||
}
|
||||
|
||||
// static
|
||||
bool FirstRunWizard::CreateMacro(std::shared_ptr<Macro> ¯o,
|
||||
const std::string ¯oName,
|
||||
const std::string &conditionId,
|
||||
obs_data_t *conditionData,
|
||||
const std::string &actionId,
|
||||
obs_data_t *actionData)
|
||||
{
|
||||
// 1. Create and register the Macro
|
||||
macro = std::make_shared<Macro>(macroName, GetGlobalMacroSettings());
|
||||
if (!macro) {
|
||||
blog(LOG_WARNING, "FirstRunWizard: Macro allocation failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Instantiate condition via factory, then hydrate via Load()
|
||||
auto condition =
|
||||
MacroConditionFactory::Create(conditionId, macro.get());
|
||||
if (!condition) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: condition factory returned null "
|
||||
"for id '%s' — is the base plugin loaded?",
|
||||
conditionId.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!condition->Load(conditionData)) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: condition Load() failed for id '%s'",
|
||||
conditionId.c_str());
|
||||
return false;
|
||||
}
|
||||
macro->Conditions().emplace_back(condition);
|
||||
|
||||
// 3. Instantiate action via factory, then hydrate via Load()
|
||||
auto action = MacroActionFactory::Create(actionId, macro.get());
|
||||
if (!action) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: action factory returned null "
|
||||
"for id '%s' — is the base plugin loaded?",
|
||||
actionId.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!action->Load(actionData)) {
|
||||
blog(LOG_WARNING,
|
||||
"FirstRunWizard: action Load() failed for id '%s'",
|
||||
actionId.c_str());
|
||||
return false;
|
||||
}
|
||||
macro->Actions().emplace_back(action);
|
||||
|
||||
blog(LOG_INFO, "FirstRunWizard: created macro '%s'", macroName.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace wiz
|
||||
} // namespace advss
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTimer>
|
||||
#include <QRadioButton>
|
||||
#include <QWizard>
|
||||
#include <QWizardPage>
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace advss {
|
||||
|
||||
bool IsFirstRun();
|
||||
|
||||
class Macro;
|
||||
|
||||
bool IsFirstRun();
|
||||
namespace wiz {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Page IDs
|
||||
// ---------------------------------------------------------------------------
|
||||
enum WizardPageId {
|
||||
PAGE_WELCOME = 0,
|
||||
PAGE_SCENE,
|
||||
PAGE_WINDOW,
|
||||
PAGE_REVIEW,
|
||||
PAGE_TEMPLATE,
|
||||
PAGE_WINDOW_SCENE,
|
||||
PAGE_WINDOW_CONDITION,
|
||||
PAGE_WINDOW_REVIEW,
|
||||
PAGE_SEQ_TRIGGER,
|
||||
PAGE_SEQ_SCENES,
|
||||
PAGE_SEQ_REVIEW,
|
||||
PAGE_AUDIO_SOURCE,
|
||||
PAGE_AUDIO_TARGET,
|
||||
PAGE_AUDIO_REVIEW,
|
||||
PAGE_DONE,
|
||||
};
|
||||
|
||||
@@ -36,64 +39,23 @@ class WelcomePage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WelcomePage(QWidget *parent = nullptr);
|
||||
int nextId() const override { return PAGE_SCENE; }
|
||||
int nextId() const override { return PAGE_TEMPLATE; }
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SceneSelectionPage
|
||||
// Registers wizard field "targetScene" (QString).
|
||||
// TemplatePage
|
||||
// Lets the user choose which kind of automation to create.
|
||||
// ---------------------------------------------------------------------------
|
||||
class SceneSelectionPage : public QWizardPage {
|
||||
class TemplatePage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SceneSelectionPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_WINDOW; }
|
||||
explicit TemplatePage(QWidget *parent = nullptr);
|
||||
int nextId() const override;
|
||||
|
||||
private:
|
||||
QComboBox *_sceneCombo;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WindowConditionPage
|
||||
// Registers wizard field "windowTitle" (QString).
|
||||
// Auto-detect button samples the focused window after a countdown.
|
||||
// ---------------------------------------------------------------------------
|
||||
class WindowConditionPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WindowConditionPage(QWidget *parent = nullptr);
|
||||
void initializePage() override;
|
||||
bool isComplete() const override;
|
||||
int nextId() const override { return PAGE_REVIEW; }
|
||||
|
||||
private slots:
|
||||
void onAutoDetectClicked();
|
||||
void onCountdownTick();
|
||||
|
||||
private:
|
||||
QLineEdit *_windowEdit;
|
||||
QPushButton *_autoDetect;
|
||||
QTimer *_detectTimer;
|
||||
int _countdown = 3;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ReviewPage
|
||||
// Displays a summary and calls FirstRunWizard::CreateMacro() on Finish.
|
||||
// ---------------------------------------------------------------------------
|
||||
class ReviewPage : public QWizardPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ReviewPage(QWidget *parent, std::shared_ptr<Macro> ¯o);
|
||||
void initializePage() override;
|
||||
bool validatePage() override;
|
||||
int nextId() const override { return PAGE_DONE; }
|
||||
|
||||
private:
|
||||
QLabel *_summary;
|
||||
std::shared_ptr<Macro> &_macro;
|
||||
QRadioButton *_windowRadio;
|
||||
QRadioButton *_sequenceRadio;
|
||||
QRadioButton *_audioRadio;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -113,17 +75,14 @@ class FirstRunWizard : public QWizard {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FirstRunWizard(QWidget *parent = nullptr);
|
||||
static std::shared_ptr<Macro> ShowWizard(QWidget *parent,
|
||||
bool *wasSkipped = nullptr);
|
||||
static bool
|
||||
CreateMacro(std::shared_ptr<Macro> ¯o, const std::string ¯oName,
|
||||
const std::string &conditionId, obs_data_t *conditionData,
|
||||
const std::string &actionId, obs_data_t *actionData);
|
||||
static std::shared_ptr<advss::Macro>
|
||||
ShowWizard(QWidget *parent, bool *wasSkipped = nullptr);
|
||||
|
||||
private:
|
||||
void markFirstRunComplete();
|
||||
|
||||
std::shared_ptr<Macro> _macro;
|
||||
std::shared_ptr<advss::Macro> _macro;
|
||||
};
|
||||
|
||||
} // namespace wiz
|
||||
} // namespace advss
|
||||
|
||||
@@ -99,4 +99,23 @@ std::optional<std::string> AccessJsonArrayIndex(const std::string &jsonStr,
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
ExtractSingleJsonArrayElement(const std::string &jsonStr)
|
||||
{
|
||||
try {
|
||||
nlohmann::json json = nlohmann::json::parse(jsonStr);
|
||||
if (!json.is_array() || json.size() != 1) {
|
||||
return {};
|
||||
}
|
||||
auto result = json.at(0);
|
||||
if (result.is_string()) {
|
||||
return result.get<std::string>();
|
||||
}
|
||||
return result.dump();
|
||||
} catch (const nlohmann::json::exception &) {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -17,5 +17,7 @@ EXPORT std::optional<std::string> QueryJson(const std::string &json,
|
||||
const std::string &query);
|
||||
EXPORT std::optional<std::string> AccessJsonArrayIndex(const std::string &json,
|
||||
const int index);
|
||||
EXPORT std::optional<std::string>
|
||||
ExtractSingleJsonArrayElement(const std::string &json);
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "priority-helper.hpp"
|
||||
#include "switcher-data.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "scene-group.hpp"
|
||||
#include "switch-audio.hpp"
|
||||
#include "switch-executable.hpp"
|
||||
@@ -10,15 +12,16 @@
|
||||
#include "switch-pause.hpp"
|
||||
#include "switch-random.hpp"
|
||||
#include "switch-screen-region.hpp"
|
||||
#include "switch-sequence.hpp"
|
||||
#include "switch-time.hpp"
|
||||
#include "switch-transitions.hpp"
|
||||
#include "switch-window.hpp"
|
||||
#include "switch-sequence.hpp"
|
||||
#include "switch-video.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "switch-window.hpp"
|
||||
#include "switcher-data.hpp"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include <algorithm>
|
||||
#include <QThread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <export-symbol-helper.hpp>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QParallelAnimationGroup>
|
||||
@@ -8,7 +10,7 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
class Section : public QWidget {
|
||||
class ADVSS_EXPORT Section : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QStandardItemModel>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -42,34 +43,46 @@ static void hasFilterEnum(obs_source_t *, obs_source_t *filter, void *ptr)
|
||||
|
||||
QStringList GetSourcesWithFilterNames()
|
||||
{
|
||||
struct EnumParam {
|
||||
QStringList list;
|
||||
// Some items appear in both obs_enum_sources and
|
||||
// obs_enum_scenes, so track pointers to avoid adding them
|
||||
// twice.
|
||||
std::unordered_set<obs_source_t *> seen;
|
||||
};
|
||||
|
||||
static auto enumSourcesWithFilters = [](void *param,
|
||||
obs_source_t *source) {
|
||||
if (!source) {
|
||||
return true;
|
||||
}
|
||||
QStringList *list = reinterpret_cast<QStringList *>(param);
|
||||
auto *ep = reinterpret_cast<EnumParam *>(param);
|
||||
const auto [_, inserted] = ep->seen.insert(source);
|
||||
if (!inserted) {
|
||||
return true;
|
||||
}
|
||||
bool hasFilter = false;
|
||||
obs_source_enum_filters(source, hasFilterEnum, &hasFilter);
|
||||
if (hasFilter) {
|
||||
*list << obs_source_get_name(source);
|
||||
ep->list << obs_source_get_name(source);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
QStringList list;
|
||||
obs_enum_sources(enumSourcesWithFilters, &list);
|
||||
EnumParam ep;
|
||||
obs_enum_sources(enumSourcesWithFilters, &ep);
|
||||
|
||||
#if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(31, 1, 0)
|
||||
obs_enum_scenes(enumSourcesWithFilters, &list);
|
||||
obs_enum_scenes(enumSourcesWithFilters, &ep);
|
||||
#else
|
||||
static const auto enumCanvases = [](void *listPtr,
|
||||
static const auto enumCanvases = [](void *param,
|
||||
obs_canvas_t *canvas) -> bool {
|
||||
obs_canvas_enum_scenes(canvas, enumSourcesWithFilters, listPtr);
|
||||
obs_canvas_enum_scenes(canvas, enumSourcesWithFilters, param);
|
||||
return true;
|
||||
};
|
||||
obs_enum_canvases(enumCanvases, &list);
|
||||
obs_enum_canvases(enumCanvases, &ep);
|
||||
#endif
|
||||
return list;
|
||||
return ep.list;
|
||||
}
|
||||
|
||||
QStringList GetMediaSourceNames()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "temp-variable.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "macro-action-macro.hpp"
|
||||
#include "macro-edit.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-segment.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "non-modal-dialog.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QComboBox>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsColorizeEffect>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QScrollBar>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "export-symbol-helper.hpp"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QColor>
|
||||
#include <QComboBox>
|
||||
#include <QIcon>
|
||||
#include <QListWidget>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
|
||||
class QAbstractButton;
|
||||
class QComboBox;
|
||||
class QListWidget;
|
||||
class QWidget;
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Returns QObject* to QPropertyAnimation object
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <QCursor>
|
||||
#include <QListWidget>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "export-symbol-helper.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <QListWidget>
|
||||
#include <QMetaObject>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
|
||||
class QListWidget;
|
||||
class QObject;
|
||||
class QWidget;
|
||||
|
||||
namespace advss {
|
||||
|
||||
EXPORT std::pair<int, int> GetCursorPos();
|
||||
|
||||
106
lib/variables/variable-color-button.cpp
Normal file
106
lib/variables/variable-color-button.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "variable-color-button.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
VariableColorButton::VariableColorButton(QWidget *parent,
|
||||
const QString &selectText)
|
||||
: QWidget(parent),
|
||||
_colorSwatch(new QLabel()),
|
||||
_selectColor(new QPushButton(selectText)),
|
||||
_variable(new VariableSelection(this)),
|
||||
_toggleType(new QPushButton())
|
||||
{
|
||||
_toggleType->setCheckable(true);
|
||||
_toggleType->setMaximumWidth(11);
|
||||
SetButtonIcon(_toggleType, GetThemeTypeName() == "Light"
|
||||
? ":/res/images/dots-vert.svg"
|
||||
: "theme:Dark/dots-vert.svg");
|
||||
|
||||
QWidget::connect(_selectColor, SIGNAL(clicked()), this,
|
||||
SLOT(SelectColorClicked()));
|
||||
QWidget::connect(_toggleType, SIGNAL(toggled(bool)), this,
|
||||
SLOT(ToggleTypeClicked(bool)));
|
||||
QWidget::connect(_variable, SIGNAL(SelectionChanged(const QString &)),
|
||||
this, SLOT(VariableChanged(const QString &)));
|
||||
|
||||
auto layout = new QHBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(_colorSwatch);
|
||||
layout->addWidget(_selectColor);
|
||||
layout->addWidget(_variable);
|
||||
layout->addWidget(_toggleType);
|
||||
setLayout(layout);
|
||||
SetVisibility();
|
||||
}
|
||||
|
||||
void VariableColorButton::SetValue(const ColorVariable &color)
|
||||
{
|
||||
_color = color;
|
||||
const QSignalBlocker b1(_toggleType);
|
||||
const QSignalBlocker b2(_variable);
|
||||
_toggleType->setChecked(!color.IsFixedType());
|
||||
SetupColorLabel(color.GetFixedValue());
|
||||
_variable->SetVariable(color.GetVariable());
|
||||
SetVisibility();
|
||||
}
|
||||
|
||||
void VariableColorButton::SelectColorClicked()
|
||||
{
|
||||
const QColor color = QColorDialog::getColor(
|
||||
_color.GetFixedValue(), this, _selectColor->text(),
|
||||
QColorDialog::ColorDialogOption());
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
SetupColorLabel(color);
|
||||
_color._value = color;
|
||||
emit ColorVariableChanged(_color);
|
||||
}
|
||||
|
||||
void VariableColorButton::VariableChanged(const QString &name)
|
||||
{
|
||||
_color._variable = GetWeakVariableByQString(name);
|
||||
emit ColorVariableChanged(_color);
|
||||
}
|
||||
|
||||
void VariableColorButton::ToggleTypeClicked(bool useVariable)
|
||||
{
|
||||
_color._type = useVariable ? ColorVariable::Type::VARIABLE
|
||||
: ColorVariable::Type::FIXED_VALUE;
|
||||
SetVisibility();
|
||||
emit ColorVariableChanged(_color);
|
||||
}
|
||||
|
||||
void VariableColorButton::SetupColorLabel(const QColor &color)
|
||||
{
|
||||
_colorSwatch->setText(color.name());
|
||||
_colorSwatch->setStyleSheet(
|
||||
QString("background-color: %1;").arg(color.name()));
|
||||
}
|
||||
|
||||
void VariableColorButton::SetVisibility()
|
||||
{
|
||||
if (_color.IsFixedType()) {
|
||||
SetupColorLabel(_color.GetFixedValue());
|
||||
_colorSwatch->show();
|
||||
_selectColor->show();
|
||||
_variable->hide();
|
||||
_toggleType->setVisible(!GetVariables().empty());
|
||||
} else {
|
||||
_colorSwatch->hide();
|
||||
_selectColor->hide();
|
||||
_variable->show();
|
||||
_variable->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorVariableTooltip"));
|
||||
_toggleType->show();
|
||||
}
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
37
lib/variables/variable-color-button.hpp
Normal file
37
lib/variables/variable-color-button.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include "export-symbol-helper.hpp"
|
||||
#include "variable-color.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class ADVSS_EXPORT VariableColorButton : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
VariableColorButton(QWidget *parent, const QString &selectText);
|
||||
void SetValue(const ColorVariable &);
|
||||
ColorVariable Value() const { return _color; }
|
||||
|
||||
public slots:
|
||||
void SelectColorClicked();
|
||||
void VariableChanged(const QString &);
|
||||
void ToggleTypeClicked(bool useVariable);
|
||||
|
||||
signals:
|
||||
void ColorVariableChanged(const ColorVariable &);
|
||||
|
||||
private:
|
||||
void SetupColorLabel(const QColor &);
|
||||
void SetVisibility();
|
||||
|
||||
ColorVariable _color;
|
||||
QLabel *_colorSwatch;
|
||||
QPushButton *_selectColor;
|
||||
VariableSelection *_variable;
|
||||
QPushButton *_toggleType;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
53
lib/variables/variable-color.cpp
Normal file
53
lib/variables/variable-color.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "variable-color.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
ColorVariable::ColorVariable(const QColor &value) : _value(value) {}
|
||||
|
||||
void ColorVariable::Save(obs_data_t *obj, const char *name) const
|
||||
{
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
obs_data_set_int(data, "version", 1);
|
||||
obs_data_set_int(data, "type", static_cast<int>(_type));
|
||||
auto var = _variable.lock();
|
||||
if (var) {
|
||||
obs_data_set_string(data, "variable", var->Name().c_str());
|
||||
}
|
||||
obs_data_set_int(data, "red", _value.red());
|
||||
obs_data_set_int(data, "green", _value.green());
|
||||
obs_data_set_int(data, "blue", _value.blue());
|
||||
obs_data_set_obj(obj, name, data);
|
||||
}
|
||||
|
||||
void ColorVariable::Load(obs_data_t *obj, const char *name)
|
||||
{
|
||||
OBSDataAutoRelease data = obs_data_get_obj(obj, name);
|
||||
_value.setRed(obs_data_get_int(data, "red"));
|
||||
_value.setGreen(obs_data_get_int(data, "green"));
|
||||
_value.setBlue(obs_data_get_int(data, "blue"));
|
||||
if (!obs_data_has_user_value(data, "version")) {
|
||||
// Old format: no variable support, just R/G/B
|
||||
_type = Type::FIXED_VALUE;
|
||||
return;
|
||||
}
|
||||
auto variableName = obs_data_get_string(data, "variable");
|
||||
_variable = GetWeakVariableByName(variableName);
|
||||
_type = static_cast<Type>(obs_data_get_int(data, "type"));
|
||||
}
|
||||
|
||||
QColor ColorVariable::GetValue() const
|
||||
{
|
||||
if (_type == Type::FIXED_VALUE) {
|
||||
return _value;
|
||||
}
|
||||
auto var = _variable.lock();
|
||||
if (!var) {
|
||||
return Qt::black;
|
||||
}
|
||||
const QColor color(QString::fromStdString(var->Value()));
|
||||
return color.isValid() ? color : Qt::black;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
35
lib/variables/variable-color.hpp
Normal file
35
lib/variables/variable-color.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <obs-data.h>
|
||||
#include <QColor>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class VariableColorButton;
|
||||
|
||||
class ADVSS_EXPORT ColorVariable {
|
||||
public:
|
||||
enum class Type { FIXED_VALUE, VARIABLE };
|
||||
|
||||
ColorVariable() = default;
|
||||
ColorVariable(const QColor &value);
|
||||
|
||||
void Save(obs_data_t *obj, const char *name) const;
|
||||
void Load(obs_data_t *obj, const char *name);
|
||||
|
||||
QColor GetValue() const;
|
||||
QColor GetFixedValue() const { return _value; }
|
||||
bool IsFixedType() const { return _type == Type::FIXED_VALUE; }
|
||||
Type GetType() const { return _type; }
|
||||
std::weak_ptr<Variable> GetVariable() const { return _variable; }
|
||||
|
||||
private:
|
||||
Type _type = Type::FIXED_VALUE;
|
||||
QColor _value = Qt::black;
|
||||
std::weak_ptr<Variable> _variable;
|
||||
|
||||
friend class VariableColorButton;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -18,6 +18,30 @@ static std::deque<std::shared_ptr<Item>> variables;
|
||||
static std::mutex lastVariableChangeMutex;
|
||||
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
|
||||
|
||||
// When set, Variable::Value() and Variable::SetValue() operate on this context
|
||||
// instead of the global variable state. Used by action queues to isolate
|
||||
// variable reads and writes to a snapshot taken at the time the action was
|
||||
// added to the queue, so that actions can modify variables without affecting
|
||||
// the global state or other queue entries.
|
||||
thread_local static VariableContext *activeVarContext = nullptr;
|
||||
|
||||
VariableContext CreateVariableContext()
|
||||
{
|
||||
VariableContext context;
|
||||
for (const auto &v : variables) {
|
||||
const auto &var = std::dynamic_pointer_cast<Variable>(v);
|
||||
if (var) {
|
||||
context[var->Name()] = var->Value(false);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
void SetActiveVariableContext(VariableContext *context)
|
||||
{
|
||||
activeVarContext = context;
|
||||
}
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddEarlySaveStep(SaveVariables);
|
||||
@@ -76,6 +100,14 @@ void Variable::Save(obs_data_t *obj) const
|
||||
|
||||
std::string Variable::Value(bool updateLastUsed) const
|
||||
{
|
||||
if (activeVarContext) {
|
||||
auto it = activeVarContext->find(Name());
|
||||
if (it == activeVarContext->end()) {
|
||||
return "";
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (updateLastUsed) {
|
||||
UpdateLastUsed();
|
||||
@@ -108,16 +140,31 @@ std::optional<int> Variable::IntValue() const
|
||||
|
||||
void Variable::SetValue(const std::string &value)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_previousValue = _value;
|
||||
_value = value;
|
||||
|
||||
UpdateLastUsed();
|
||||
if (_previousValue != _value) {
|
||||
_lastChanged = std::chrono::high_resolution_clock::now();
|
||||
++_valueChangeCount;
|
||||
if (activeVarContext) {
|
||||
auto it = activeVarContext->find(Name());
|
||||
if (it == activeVarContext->end()) {
|
||||
return;
|
||||
}
|
||||
it->second = value;
|
||||
setLastVariableChangeTime();
|
||||
return;
|
||||
}
|
||||
setLastVariableChangeTime();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_previousValue = _value;
|
||||
_value = value;
|
||||
|
||||
UpdateLastUsed();
|
||||
if (_previousValue != _value) {
|
||||
_lastChanged =
|
||||
std::chrono::high_resolution_clock::now();
|
||||
++_valueChangeCount;
|
||||
}
|
||||
setLastVariableChangeTime();
|
||||
}
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
void Variable::SetValue(double value)
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
#include "item-selection-helpers.hpp"
|
||||
#include "resizing-text-edit.hpp"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <obs-data.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <QStringList>
|
||||
|
||||
namespace advss {
|
||||
@@ -43,6 +45,11 @@ public:
|
||||
std::optional<uint64_t> GetSecondsSinceLastUse() const;
|
||||
std::optional<uint64_t> GetSecondsSinceLastChange() const;
|
||||
void MarkAsUsed() const;
|
||||
EXPORT std::condition_variable &GetCV() { return _cv; }
|
||||
EXPORT std::unique_lock<std::mutex> GetCVLock()
|
||||
{
|
||||
return std::unique_lock<std::mutex>(_cvMutex);
|
||||
}
|
||||
|
||||
private:
|
||||
SaveAction _saveAction = SaveAction::DONT_SAVE;
|
||||
@@ -53,6 +60,8 @@ private:
|
||||
mutable std::chrono::high_resolution_clock::time_point _lastUsed;
|
||||
mutable std::chrono::high_resolution_clock::time_point _lastChanged;
|
||||
mutable std::mutex _mutex;
|
||||
std::mutex _cvMutex;
|
||||
std::condition_variable _cv;
|
||||
|
||||
void UpdateLastUsed() const;
|
||||
|
||||
@@ -108,6 +117,10 @@ signals:
|
||||
void Remove(const QString &);
|
||||
};
|
||||
|
||||
using VariableContext = std::unordered_map<std::string, std::string>;
|
||||
VariableContext CreateVariableContext();
|
||||
void SetActiveVariableContext(VariableContext *context);
|
||||
|
||||
std::deque<std::shared_ptr<Item>> &GetVariables();
|
||||
EXPORT Variable *GetVariableByName(const std::string &name);
|
||||
EXPORT Variable *GetVariableByQString(const QString &name);
|
||||
|
||||
@@ -41,6 +41,7 @@ add_plugin(stream-deck)
|
||||
add_plugin(twitch)
|
||||
add_plugin(usb)
|
||||
add_plugin(video)
|
||||
add_plugin(speech)
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@@ -165,12 +165,20 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionRecord> entryData)
|
||||
: QWidget(parent),
|
||||
_actions(new QComboBox()),
|
||||
_pauseHint(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.action.recording.pause.hint"))),
|
||||
_splitHint(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.action.recording.split.hint"))),
|
||||
_pauseHint(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.recording.pause.hint"),
|
||||
this)),
|
||||
_splitHint(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.recording.split.hint"),
|
||||
this)),
|
||||
_recordFolder(new FileSelection(FileSelection::Type::FOLDER, this)),
|
||||
_recordFileFormat(new VariableLineEdit(this)),
|
||||
_outputNotActiveHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.recording.outputNotActive.hint"),
|
||||
this)),
|
||||
_chapterName(new VariableLineEdit(this))
|
||||
{
|
||||
populateActionSelection(_actions);
|
||||
@@ -184,16 +192,18 @@ MacroActionRecordEdit::MacroActionRecordEdit(
|
||||
QWidget::connect(_chapterName, SIGNAL(editingFinished()), this,
|
||||
SLOT(ChapterNameChanged()));
|
||||
|
||||
auto mainLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text("AdvSceneSwitcher.action.recording.entry"),
|
||||
mainLayout,
|
||||
{{"{{actions}}", _actions},
|
||||
{"{{pauseHint}}", _pauseHint},
|
||||
{"{{splitHint}}", _splitHint},
|
||||
{"{{recordFolder}}", _recordFolder},
|
||||
{"{{recordFileFormat}}", _recordFileFormat},
|
||||
{"{{chapterName}}", _chapterName}});
|
||||
setLayout(mainLayout);
|
||||
_mainLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.action.recording.layout"),
|
||||
_mainLayout,
|
||||
{{"{{actions}}", _actions},
|
||||
{"{{pauseHint}}", _pauseHint},
|
||||
{"{{splitHint}}", _splitHint},
|
||||
{"{{recordFolder}}", _recordFolder},
|
||||
{"{{recordFileFormat}}", _recordFileFormat},
|
||||
{"{{outputNotActiveHelp}}", _outputNotActiveHelp},
|
||||
{"{{chapterName}}", _chapterName}});
|
||||
setLayout(_mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
@@ -245,8 +255,20 @@ void MacroActionRecordEdit::SetWidgetVisibility()
|
||||
MacroActionRecord::Action::FOLDER);
|
||||
_recordFileFormat->setVisible(_entryData->_action ==
|
||||
MacroActionRecord::Action::FILE_FORMAT);
|
||||
_outputNotActiveHelp->setVisible(
|
||||
_entryData->_action == MacroActionRecord::Action::FOLDER ||
|
||||
_entryData->_action == MacroActionRecord::Action::FILE_FORMAT);
|
||||
_chapterName->setVisible(_entryData->_action ==
|
||||
MacroActionRecord::Action::ADD_CHAPTER);
|
||||
const bool hasExtraWidget =
|
||||
_entryData->_action == MacroActionRecord::Action::FOLDER ||
|
||||
_entryData->_action == MacroActionRecord::Action::FILE_FORMAT ||
|
||||
_entryData->_action == MacroActionRecord::Action::ADD_CHAPTER;
|
||||
if (hasExtraWidget) {
|
||||
RemoveStretchIfPresent(_mainLayout);
|
||||
} else {
|
||||
AddStretchIfNecessary(_mainLayout);
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionRecordEdit::ActionChanged(int value)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "file-selection.hpp"
|
||||
#include "help-icon.hpp"
|
||||
#include "variable-line-edit.hpp"
|
||||
|
||||
#include <QDir>
|
||||
@@ -64,19 +65,19 @@ private slots:
|
||||
void FormatStringChanged();
|
||||
void ChapterNameChanged();
|
||||
|
||||
protected:
|
||||
QComboBox *_actions;
|
||||
QLabel *_pauseHint;
|
||||
QLabel *_splitHint;
|
||||
FileSelection *_recordFolder;
|
||||
VariableLineEdit *_recordFileFormat;
|
||||
VariableLineEdit *_chapterName;
|
||||
std::shared_ptr<MacroActionRecord> _entryData;
|
||||
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
|
||||
QComboBox *_actions;
|
||||
HelpIcon *_pauseHint;
|
||||
HelpIcon *_splitHint;
|
||||
FileSelection *_recordFolder;
|
||||
VariableLineEdit *_recordFileFormat;
|
||||
HelpIcon *_outputNotActiveHelp;
|
||||
VariableLineEdit *_chapterName;
|
||||
std::shared_ptr<MacroActionRecord> _entryData;
|
||||
QHBoxLayout *_mainLayout;
|
||||
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "macro-action-scene-visibility.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "transition-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
@@ -158,12 +160,11 @@ static void attachRestoreContext(obs_sceneitem_t *item,
|
||||
signal_handler_connect(sh, "destroy", handleSourceDestroyed, ctx);
|
||||
}
|
||||
|
||||
static void setSceneItemVisibility(obs_sceneitem_t *item,
|
||||
const bool setTransition,
|
||||
const OBSWeakSource &transitionWeak,
|
||||
const bool setDuration,
|
||||
const Duration &duration,
|
||||
MacroActionSceneVisibility::Action action)
|
||||
static obs_source_t *
|
||||
setSceneItemVisibility(obs_sceneitem_t *item, const bool setTransition,
|
||||
const OBSWeakSource &transitionWeak,
|
||||
const bool setDuration, const Duration &duration,
|
||||
MacroActionSceneVisibility::Action action)
|
||||
{
|
||||
const OBSSourceAutoRelease transition = OBSGetStrongRef(transitionWeak);
|
||||
const bool itemIsVisible = obs_sceneitem_visible(item);
|
||||
@@ -200,7 +201,7 @@ static void setSceneItemVisibility(obs_sceneitem_t *item,
|
||||
}
|
||||
|
||||
if (!setTransition && !setDuration) {
|
||||
return;
|
||||
return privateTransitionSource;
|
||||
}
|
||||
|
||||
if (!privateTransitionSource) {
|
||||
@@ -213,27 +214,151 @@ static void setSceneItemVisibility(obs_sceneitem_t *item,
|
||||
item, !itemIsVisible,
|
||||
currentTransitionDuration);
|
||||
}
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto sh = obs_source_get_signal_handler(privateTransitionSource);
|
||||
if (!sh) {
|
||||
return;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
attachRestoreContext(item, privateTransitionSource, itemIsVisible,
|
||||
currentTransition, currentTransitionDuration);
|
||||
|
||||
return privateTransitionSource;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct TransitionWaitItem {
|
||||
obs_sceneitem_t *sceneItem;
|
||||
obs_source_t *expectedSource;
|
||||
bool show;
|
||||
std::atomic<int> *pendingCount;
|
||||
std::atomic<bool> done{false};
|
||||
|
||||
void markDone()
|
||||
{
|
||||
bool expected = false;
|
||||
if (done.compare_exchange_strong(expected, true)) {
|
||||
(*pendingCount)--;
|
||||
GetMacroTransitionCV().notify_all();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void onTransitionDone(void *data, calldata_t *)
|
||||
{
|
||||
static_cast<TransitionWaitItem *>(data)->markDone();
|
||||
}
|
||||
|
||||
static void waitForSceneItemTransitions(
|
||||
std::atomic<int> &pendingCount,
|
||||
const std::vector<std::unique_ptr<TransitionWaitItem>> &waitItems,
|
||||
Macro *macro)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
SetMacroAbortWait(false);
|
||||
std::unique_lock<std::mutex> lock(*GetMutex());
|
||||
while (pendingCount > 0 && !MacroWaitShouldAbort() &&
|
||||
!MacroIsStopped(macro) && !OBSIsShuttingDown()) {
|
||||
GetMacroTransitionCV().wait_for(lock, 100ms);
|
||||
// Detect transitions replaced mid-wait (e.g. user sets
|
||||
// transition to "None" via OBS context menu). In that
|
||||
// case transition_stop never fires, so we poll.
|
||||
for (const auto &wi : waitItems) {
|
||||
if (wi->done) {
|
||||
continue;
|
||||
}
|
||||
obs_source_t *current = obs_sceneitem_get_transition(
|
||||
wi->sceneItem, wi->show);
|
||||
if (current != wi->expectedSource) {
|
||||
wi->markDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionSceneVisibility::PerformAction()
|
||||
{
|
||||
auto items = _source.GetSceneItems(_scene);
|
||||
for (const auto &item : items) {
|
||||
setSceneItemVisibility(item, _updateTransition,
|
||||
_transition.GetTransition(),
|
||||
_updateDuration, _duration, _action);
|
||||
|
||||
if (!_blockUntilTransitionDone) {
|
||||
for (const auto &item : items) {
|
||||
setSceneItemVisibility(item, _updateTransition,
|
||||
_transition.GetTransition(),
|
||||
_updateDuration, _duration,
|
||||
_action);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
std::atomic<int> pendingCount(0);
|
||||
std::vector<OBSSourceAutoRelease> transitionSources;
|
||||
std::vector<std::unique_ptr<TransitionWaitItem>> waitItems;
|
||||
std::vector<OBSSignal> signalConnections;
|
||||
|
||||
for (const auto &item : items) {
|
||||
const bool itemWasVisible = obs_sceneitem_visible(item);
|
||||
|
||||
bool targetVisible;
|
||||
switch (_action) {
|
||||
case Action::SHOW:
|
||||
targetVisible = true;
|
||||
break;
|
||||
case Action::HIDE:
|
||||
targetVisible = false;
|
||||
break;
|
||||
case Action::TOGGLE:
|
||||
targetVisible = !itemWasVisible;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemWasVisible == targetVisible) {
|
||||
// No-op: visibility won't change, no transition will play
|
||||
setSceneItemVisibility(item, _updateTransition,
|
||||
_transition.GetTransition(),
|
||||
_updateDuration, _duration,
|
||||
_action);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *source = setSceneItemVisibility(
|
||||
item, _updateTransition, _transition.GetTransition(),
|
||||
_updateDuration, _duration, _action);
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *sh = obs_source_get_signal_handler(source);
|
||||
if (!sh) {
|
||||
continue;
|
||||
}
|
||||
|
||||
transitionSources.emplace_back(obs_source_get_ref(source));
|
||||
++pendingCount;
|
||||
|
||||
auto wi = std::make_unique<TransitionWaitItem>();
|
||||
wi->sceneItem = item;
|
||||
wi->expectedSource = source;
|
||||
wi->show = !itemWasVisible;
|
||||
wi->pendingCount = &pendingCount;
|
||||
|
||||
signalConnections.emplace_back(sh, "transition_stop",
|
||||
onTransitionDone, wi.get());
|
||||
waitItems.push_back(std::move(wi));
|
||||
}
|
||||
|
||||
if (pendingCount == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
waitForSceneItemTransitions(pendingCount, waitItems, GetMacro());
|
||||
return !MacroWaitShouldAbort();
|
||||
}
|
||||
|
||||
void MacroActionSceneVisibility::LogAction() const
|
||||
@@ -261,6 +386,8 @@ bool MacroActionSceneVisibility::Save(obs_data_t *obj) const
|
||||
obs_data_set_bool(obj, "updateDuration", _updateDuration);
|
||||
_duration.Save(obj);
|
||||
obs_data_set_int(obj, "action", static_cast<int>(_action));
|
||||
obs_data_set_bool(obj, "blockUntilTransitionDone",
|
||||
_blockUntilTransitionDone);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -282,6 +409,8 @@ bool MacroActionSceneVisibility::Load(obs_data_t *obj)
|
||||
_duration.Load(obj);
|
||||
_action = static_cast<MacroActionSceneVisibility::Action>(
|
||||
obs_data_get_int(obj, "action"));
|
||||
_blockUntilTransitionDone =
|
||||
obs_data_get_bool(obj, "blockUntilTransitionDone");
|
||||
|
||||
// TODO: Remove in future version
|
||||
if (obs_data_get_int(obj, "sourceType") != 0) {
|
||||
@@ -344,6 +473,10 @@ MacroActionSceneVisibilityEdit::MacroActionSceneVisibilityEdit(
|
||||
_updateDuration(new QCheckBox(this)),
|
||||
_duration(new DurationSelection(this, false)),
|
||||
_durationLayout(new QHBoxLayout),
|
||||
_blockUntilTransitionDone(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.sceneVisibility.blockUntilTransitionDone"),
|
||||
this)),
|
||||
_actions(new QComboBox())
|
||||
{
|
||||
populateActionSelection(_actions);
|
||||
@@ -369,6 +502,8 @@ MacroActionSceneVisibilityEdit::MacroActionSceneVisibilityEdit(
|
||||
SLOT(UpdateDurationChanged(int)));
|
||||
QWidget::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
|
||||
this, SLOT(DurationChanged(const Duration &)));
|
||||
QWidget::connect(_blockUntilTransitionDone, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(BlockUntilTransitionDoneChanged(int)));
|
||||
|
||||
auto sceneItemLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
@@ -397,6 +532,7 @@ MacroActionSceneVisibilityEdit::MacroActionSceneVisibilityEdit(
|
||||
layout->addLayout(sceneItemLayout);
|
||||
layout->addLayout(transitionLayout);
|
||||
layout->addLayout(_durationLayout);
|
||||
layout->addWidget(_blockUntilTransitionDone);
|
||||
setLayout(layout);
|
||||
|
||||
_entryData = entryData;
|
||||
@@ -418,6 +554,8 @@ void MacroActionSceneVisibilityEdit::UpdateEntryData()
|
||||
_transitions->SetTransition(_entryData->_transition);
|
||||
_updateDuration->setChecked(_entryData->_updateDuration);
|
||||
_duration->SetDuration(_entryData->_duration);
|
||||
_blockUntilTransitionDone->setChecked(
|
||||
_entryData->_blockUntilTransitionDone);
|
||||
}
|
||||
|
||||
void MacroActionSceneVisibilityEdit::SceneChanged(const SceneSelection &s)
|
||||
@@ -472,6 +610,12 @@ void MacroActionSceneVisibilityEdit::ActionChanged(int value)
|
||||
static_cast<MacroActionSceneVisibility::Action>(value);
|
||||
}
|
||||
|
||||
void MacroActionSceneVisibilityEdit::BlockUntilTransitionDoneChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_blockUntilTransitionDone = state;
|
||||
}
|
||||
|
||||
void MacroActionSceneVisibilityEdit::SetWidgetVisibility()
|
||||
{
|
||||
const bool hideDurationSelection =
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
TransitionSelection _transition;
|
||||
bool _updateDuration = false;
|
||||
Duration _duration;
|
||||
bool _blockUntilTransitionDone = true;
|
||||
|
||||
enum class Action {
|
||||
SHOW,
|
||||
@@ -64,6 +65,7 @@ private slots:
|
||||
void UpdateDurationChanged(int);
|
||||
void DurationChanged(const Duration &seconds);
|
||||
void ActionChanged(int value);
|
||||
void BlockUntilTransitionDoneChanged(int state);
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
@@ -77,6 +79,7 @@ private:
|
||||
QCheckBox *_updateDuration;
|
||||
DurationSelection *_duration;
|
||||
QHBoxLayout *_durationLayout;
|
||||
QCheckBox *_blockUntilTransitionDone;
|
||||
QComboBox *_actions;
|
||||
|
||||
std::shared_ptr<MacroActionSceneVisibility> _entryData;
|
||||
|
||||
@@ -19,6 +19,21 @@ static const std::map<MacroActionWait::Type, std::string> waitTypes = {
|
||||
"AdvSceneSwitcher.action.wait.type.fixed"},
|
||||
{MacroActionWait::Type::RANDOM,
|
||||
"AdvSceneSwitcher.action.wait.type.random"},
|
||||
{MacroActionWait::Type::VARIABLE_WAIT,
|
||||
"AdvSceneSwitcher.action.wait.type.variableCondition"},
|
||||
};
|
||||
|
||||
static const std::map<MacroActionWait::Condition, std::string> conditionTypes = {
|
||||
{MacroActionWait::Condition::EQUALS,
|
||||
"AdvSceneSwitcher.action.wait.condition.equals"},
|
||||
{MacroActionWait::Condition::DOES_NOT_EQUAL,
|
||||
"AdvSceneSwitcher.action.wait.condition.doesNotEqual"},
|
||||
{MacroActionWait::Condition::IS_EMPTY,
|
||||
"AdvSceneSwitcher.action.wait.condition.isEmpty"},
|
||||
{MacroActionWait::Condition::LESS_THAN,
|
||||
"AdvSceneSwitcher.action.wait.condition.lessThan"},
|
||||
{MacroActionWait::Condition::GREATER_THAN,
|
||||
"AdvSceneSwitcher.action.wait.condition.greaterThan"},
|
||||
};
|
||||
|
||||
static std::random_device rd;
|
||||
@@ -35,7 +50,52 @@ static void waitHelper(std::unique_lock<std::mutex> *lock, Macro *macro,
|
||||
}
|
||||
}
|
||||
|
||||
bool MacroActionWait::PerformAction()
|
||||
void MacroActionWait::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
if (_waitType != Type::VARIABLE_WAIT) {
|
||||
return;
|
||||
}
|
||||
AddTempvar(
|
||||
"timedOut",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.wait.timedOut"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.wait.timedOut.description"));
|
||||
}
|
||||
|
||||
bool MacroActionWait::ConditionIsMet() const
|
||||
{
|
||||
auto var = _variable.lock();
|
||||
if (!var) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (_condition) {
|
||||
case Condition::EQUALS:
|
||||
if (_regex.Enabled()) {
|
||||
return _regex.Matches(var->Value(), _strValue);
|
||||
}
|
||||
return var->Value() == std::string(_strValue);
|
||||
case Condition::DOES_NOT_EQUAL:
|
||||
if (_regex.Enabled()) {
|
||||
return !_regex.Matches(var->Value(), _strValue);
|
||||
}
|
||||
return var->Value() != std::string(_strValue);
|
||||
case Condition::IS_EMPTY:
|
||||
return var->Value().empty();
|
||||
case Condition::LESS_THAN: {
|
||||
auto val = var->DoubleValue();
|
||||
return val.has_value() && *val < (double)_numValue;
|
||||
}
|
||||
case Condition::GREATER_THAN: {
|
||||
auto val = var->DoubleValue();
|
||||
return val.has_value() && *val > (double)_numValue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MacroActionWait::PerformDurationWait()
|
||||
{
|
||||
double sleepDuration;
|
||||
if (_waitType == Type::FIXED) {
|
||||
@@ -66,20 +126,71 @@ bool MacroActionWait::PerformAction()
|
||||
return !MacroWaitShouldAbort();
|
||||
}
|
||||
|
||||
bool MacroActionWait::PerformVariableWait()
|
||||
{
|
||||
auto var = _variable.lock();
|
||||
if (!var) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto deadline =
|
||||
_useTimeout
|
||||
? std::chrono::high_resolution_clock::now() +
|
||||
std::chrono::milliseconds((
|
||||
int)(_conditionTimeout.Milliseconds()))
|
||||
: std::chrono::high_resolution_clock::time_point::max();
|
||||
|
||||
SetMacroAbortWait(false);
|
||||
SuspendLock suspendLock(*this);
|
||||
auto cvLock = var->GetCVLock();
|
||||
|
||||
while (!MacroWaitShouldAbort() && !MacroIsStopped(GetMacro())) {
|
||||
if (ConditionIsMet()) {
|
||||
SetTempVarValue("timedOut", false);
|
||||
return true;
|
||||
}
|
||||
auto now = std::chrono::high_resolution_clock::now();
|
||||
if (now >= deadline) {
|
||||
SetTempVarValue("timedOut", true);
|
||||
return true;
|
||||
}
|
||||
auto wakeAt =
|
||||
std::min(now + std::chrono::milliseconds(50), deadline);
|
||||
var->GetCV().wait_until(cvLock, wakeAt);
|
||||
}
|
||||
return !MacroWaitShouldAbort();
|
||||
}
|
||||
|
||||
bool MacroActionWait::PerformAction()
|
||||
{
|
||||
if (_waitType == Type::VARIABLE_WAIT) {
|
||||
return PerformVariableWait();
|
||||
}
|
||||
return PerformDurationWait();
|
||||
}
|
||||
|
||||
bool MacroActionWait::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
_duration.Save(obj);
|
||||
_duration2.Save(obj, "duration2");
|
||||
obs_data_set_int(obj, "waitType", static_cast<int>(_waitType));
|
||||
obs_data_set_int(obj, "version", 1);
|
||||
_duration.Save(obj);
|
||||
_duration2.Save(obj, "duration2");
|
||||
obs_data_set_string(obj, "variableName",
|
||||
GetWeakVariableName(_variable).c_str());
|
||||
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
|
||||
_strValue.Save(obj, "strValue");
|
||||
_numValue.Save(obj, "numValue");
|
||||
_regex.Save(obj);
|
||||
obs_data_set_bool(obj, "useTimeout", _useTimeout);
|
||||
_conditionTimeout.Save(obj, "conditionTimeout");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionWait::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_duration.Load(obj);
|
||||
_waitType = static_cast<Type>(obs_data_get_int(obj, "waitType"));
|
||||
// TODO: remove this fallback
|
||||
if (obs_data_get_int(obj, "version") == 1) {
|
||||
_duration2.Load(obj, "duration2");
|
||||
@@ -88,12 +199,23 @@ bool MacroActionWait::Load(obs_data_t *obj)
|
||||
_duration2.SetUnit(static_cast<Duration::Unit>(
|
||||
obs_data_get_int(obj, "displayUnit2")));
|
||||
}
|
||||
_waitType = static_cast<Type>(obs_data_get_int(obj, "waitType"));
|
||||
_duration.Load(obj);
|
||||
_variable =
|
||||
GetWeakVariableByName(obs_data_get_string(obj, "variableName"));
|
||||
_condition = static_cast<Condition>(obs_data_get_int(obj, "condition"));
|
||||
_strValue.Load(obj, "strValue");
|
||||
_numValue.Load(obj, "numValue");
|
||||
_regex.Load(obj);
|
||||
_useTimeout = obs_data_get_bool(obj, "useTimeout");
|
||||
_conditionTimeout.Load(obj, "conditionTimeout");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string MacroActionWait::GetShortDesc() const
|
||||
{
|
||||
if (_waitType == Type::VARIABLE_WAIT) {
|
||||
return GetWeakVariableName(_variable);
|
||||
}
|
||||
if (_waitType == Type::FIXED) {
|
||||
return _duration.ToString();
|
||||
}
|
||||
@@ -114,12 +236,18 @@ void MacroActionWait::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_duration.ResolveVariables();
|
||||
_duration2.ResolveVariables();
|
||||
_strValue.ResolveVariables();
|
||||
_numValue.ResolveVariables();
|
||||
_conditionTimeout.ResolveVariables();
|
||||
}
|
||||
|
||||
static inline void populateTypeSelection(QComboBox *list)
|
||||
template<typename T>
|
||||
static inline void populateSelection(QComboBox *list,
|
||||
const std::map<T, std::string> &options)
|
||||
{
|
||||
for (const auto &[_, name] : waitTypes) {
|
||||
list->addItem(obs_module_text(name.c_str()));
|
||||
for (const auto &[value, name] : options) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,10 +256,21 @@ MacroActionWaitEdit::MacroActionWaitEdit(
|
||||
: QWidget(parent),
|
||||
_duration(new DurationSelection()),
|
||||
_duration2(new DurationSelection()),
|
||||
_variable(new VariableSelection(this)),
|
||||
_variableCondition(new QComboBox()),
|
||||
_strValue(new VariableTextEdit(this, 5, 1, 1)),
|
||||
_numValue(new VariableDoubleSpinBox()),
|
||||
_regex(new RegexConfigWidget(parent)),
|
||||
_useTimeout(new QCheckBox()),
|
||||
_conditionTimeout(new DurationSelection()),
|
||||
_waitType(new QComboBox()),
|
||||
_mainLayout(new QHBoxLayout())
|
||||
_mainLayout(new QHBoxLayout()),
|
||||
_timeoutLayout(new QHBoxLayout())
|
||||
{
|
||||
populateTypeSelection(_waitType);
|
||||
_numValue->setMinimum(-9999999999);
|
||||
_numValue->setMaximum(9999999999);
|
||||
populateSelection(_waitType, waitTypes);
|
||||
populateSelection(_variableCondition, conditionTypes);
|
||||
|
||||
QWidget::connect(_duration, SIGNAL(DurationChanged(const Duration &)),
|
||||
this, SLOT(DurationChanged(const Duration &)));
|
||||
@@ -139,8 +278,37 @@ MacroActionWaitEdit::MacroActionWaitEdit(
|
||||
this, SLOT(Duration2Changed(const Duration &)));
|
||||
QWidget::connect(_waitType, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(TypeChanged(int)));
|
||||
QWidget::connect(_variable, SIGNAL(SelectionChanged(const QString &)),
|
||||
this, SLOT(VariableChanged(const QString &)));
|
||||
QWidget::connect(_variableCondition, SIGNAL(currentIndexChanged(int)),
|
||||
this, SLOT(ConditionChanged(int)));
|
||||
QWidget::connect(_strValue, SIGNAL(textChanged()), this,
|
||||
SLOT(StrValueChanged()));
|
||||
QWidget::connect(
|
||||
_numValue,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this, SLOT(NumValueChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(_regex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(RegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_useTimeout, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseTimeoutChanged(int)));
|
||||
QWidget::connect(_conditionTimeout,
|
||||
SIGNAL(DurationChanged(const Duration &)), this,
|
||||
SLOT(ConditionTimeoutChanged(const Duration &)));
|
||||
|
||||
setLayout(_mainLayout);
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.action.wait.layout.timeout"),
|
||||
_timeoutLayout,
|
||||
{
|
||||
{"{{useTimeout}}", _useTimeout},
|
||||
{"{{conditionTimeout}}", _conditionTimeout},
|
||||
});
|
||||
|
||||
auto *outerLayout = new QVBoxLayout();
|
||||
outerLayout->addLayout(_mainLayout);
|
||||
outerLayout->addLayout(_timeoutLayout);
|
||||
setLayout(outerLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
@@ -153,15 +321,30 @@ void MacroActionWaitEdit::UpdateEntryData()
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entryData->_waitType == MacroActionWait::Type::FIXED) {
|
||||
_waitType->setCurrentIndex(static_cast<int>(_entryData->_waitType));
|
||||
|
||||
switch (_entryData->_waitType) {
|
||||
case MacroActionWait::Type::FIXED:
|
||||
SetupFixedDurationEdit();
|
||||
} else {
|
||||
break;
|
||||
case MacroActionWait::Type::RANDOM:
|
||||
SetupRandomDurationEdit();
|
||||
break;
|
||||
case MacroActionWait::Type::VARIABLE_WAIT:
|
||||
SetupVariableWaitEdit();
|
||||
break;
|
||||
}
|
||||
|
||||
_duration->SetDuration(_entryData->_duration);
|
||||
_duration2->SetDuration(_entryData->_duration2);
|
||||
_waitType->setCurrentIndex(static_cast<int>(_entryData->_waitType));
|
||||
_variable->SetVariable(_entryData->_variable);
|
||||
_variableCondition->setCurrentIndex(
|
||||
static_cast<int>(_entryData->_condition));
|
||||
_strValue->setPlainText(_entryData->_strValue);
|
||||
_numValue->SetValue(_entryData->_numValue);
|
||||
_regex->SetRegexConfig(_entryData->_regex);
|
||||
_useTimeout->setChecked(_entryData->_useTimeout);
|
||||
_conditionTimeout->SetDuration(_entryData->_conditionTimeout);
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::SetupFixedDurationEdit()
|
||||
@@ -169,15 +352,24 @@ void MacroActionWaitEdit::SetupFixedDurationEdit()
|
||||
_mainLayout->removeWidget(_duration);
|
||||
_mainLayout->removeWidget(_duration2);
|
||||
_mainLayout->removeWidget(_waitType);
|
||||
_mainLayout->removeWidget(_variable);
|
||||
_mainLayout->removeWidget(_variableCondition);
|
||||
_mainLayout->removeWidget(_strValue);
|
||||
_mainLayout->removeWidget(_numValue);
|
||||
_mainLayout->removeWidget(_regex);
|
||||
ClearLayout(_mainLayout);
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{duration}}", _duration},
|
||||
{"{{waitType}}", _waitType},
|
||||
};
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.action.wait.entry.fixed"),
|
||||
_mainLayout, widgetPlaceholders);
|
||||
obs_module_text("AdvSceneSwitcher.action.wait.layout.fixed"),
|
||||
_mainLayout,
|
||||
{{"{{duration}}", _duration}, {"{{waitType}}", _waitType}});
|
||||
_duration->show();
|
||||
_duration2->hide();
|
||||
_variable->hide();
|
||||
_variableCondition->hide();
|
||||
_strValue->hide();
|
||||
_numValue->hide();
|
||||
_regex->hide();
|
||||
SetLayoutVisible(_timeoutLayout, false);
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::SetupRandomDurationEdit()
|
||||
@@ -185,30 +377,99 @@ void MacroActionWaitEdit::SetupRandomDurationEdit()
|
||||
_mainLayout->removeWidget(_duration);
|
||||
_mainLayout->removeWidget(_duration2);
|
||||
_mainLayout->removeWidget(_waitType);
|
||||
_mainLayout->removeWidget(_variable);
|
||||
_mainLayout->removeWidget(_variableCondition);
|
||||
_mainLayout->removeWidget(_strValue);
|
||||
_mainLayout->removeWidget(_numValue);
|
||||
_mainLayout->removeWidget(_regex);
|
||||
ClearLayout(_mainLayout);
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{duration}}", _duration},
|
||||
{"{{duration2}}", _duration2},
|
||||
{"{{waitType}}", _waitType},
|
||||
};
|
||||
PlaceWidgets(
|
||||
obs_module_text("AdvSceneSwitcher.action.wait.entry.random"),
|
||||
_mainLayout, widgetPlaceholders);
|
||||
obs_module_text("AdvSceneSwitcher.action.wait.layout.random"),
|
||||
_mainLayout,
|
||||
{{"{{duration}}", _duration},
|
||||
{"{{duration2}}", _duration2},
|
||||
{"{{waitType}}", _waitType}});
|
||||
_duration->show();
|
||||
_duration2->show();
|
||||
_variable->hide();
|
||||
_variableCondition->hide();
|
||||
_strValue->hide();
|
||||
_numValue->hide();
|
||||
_regex->hide();
|
||||
SetLayoutVisible(_timeoutLayout, false);
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::TypeChanged(int value)
|
||||
void MacroActionWaitEdit::SetupVariableWaitEdit()
|
||||
{
|
||||
_mainLayout->removeWidget(_duration);
|
||||
_mainLayout->removeWidget(_duration2);
|
||||
_mainLayout->removeWidget(_waitType);
|
||||
_mainLayout->removeWidget(_variable);
|
||||
_mainLayout->removeWidget(_variableCondition);
|
||||
_mainLayout->removeWidget(_strValue);
|
||||
_mainLayout->removeWidget(_numValue);
|
||||
_mainLayout->removeWidget(_regex);
|
||||
ClearLayout(_mainLayout);
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{waitType}}", _waitType},
|
||||
{"{{variable}}", _variable},
|
||||
{"{{condition}}", _variableCondition},
|
||||
{"{{strValue}}", _strValue},
|
||||
{"{{numValue}}", _numValue},
|
||||
{"{{regex}}", _regex},
|
||||
};
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.action.wait.layout.variableCondition"),
|
||||
_mainLayout, widgetPlaceholders);
|
||||
_duration->hide();
|
||||
_duration2->hide();
|
||||
_variable->show();
|
||||
_variableCondition->show();
|
||||
SetLayoutVisible(_timeoutLayout, true);
|
||||
SetVariableWaitWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::SetVariableWaitWidgetVisibility()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto cond = _entryData->_condition;
|
||||
const bool isStringComparison =
|
||||
cond == MacroActionWait::Condition::EQUALS ||
|
||||
cond == MacroActionWait::Condition::DOES_NOT_EQUAL;
|
||||
_strValue->setVisible(isStringComparison);
|
||||
_regex->setVisible(isStringComparison);
|
||||
_numValue->setVisible(cond == MacroActionWait::Condition::LESS_THAN ||
|
||||
cond == MacroActionWait::Condition::GREATER_THAN);
|
||||
_conditionTimeout->setVisible(_entryData->_useTimeout);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::TypeChanged(int idx)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
auto type = static_cast<MacroActionWait::Type>(value);
|
||||
auto type = static_cast<MacroActionWait::Type>(
|
||||
_waitType->itemData(idx).toInt());
|
||||
|
||||
if (type == MacroActionWait::Type::FIXED) {
|
||||
switch (type) {
|
||||
case MacroActionWait::Type::FIXED:
|
||||
SetupFixedDurationEdit();
|
||||
} else {
|
||||
break;
|
||||
case MacroActionWait::Type::RANDOM:
|
||||
SetupRandomDurationEdit();
|
||||
break;
|
||||
case MacroActionWait::Type::VARIABLE_WAIT:
|
||||
SetupVariableWaitEdit();
|
||||
break;
|
||||
}
|
||||
|
||||
_entryData->_waitType = type;
|
||||
_entryData->SetupTempVars();
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
@@ -229,4 +490,55 @@ void MacroActionWaitEdit::Duration2Changed(const Duration &dur)
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::VariableChanged(const QString &text)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_variable = GetWeakVariableByQString(text);
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::ConditionChanged(int idx)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_condition = static_cast<MacroActionWait::Condition>(
|
||||
_variableCondition->itemData(idx).toInt());
|
||||
SetVariableWaitWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::StrValueChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_strValue = _strValue->toPlainText().toStdString();
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::NumValueChanged(const NumberVariable<double> &val)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_numValue = val;
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::RegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_regex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::UseTimeoutChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_useTimeout = state != Qt::Unchecked;
|
||||
SetVariableWaitWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroActionWaitEdit::ConditionTimeoutChanged(const Duration &dur)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_conditionTimeout = dur;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "duration-control.hpp"
|
||||
#include "regex-config.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
#include "variable-text-edit.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -17,17 +23,40 @@ public:
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
void ResolveVariablesToFixedValues();
|
||||
|
||||
Duration _duration;
|
||||
Duration _duration2;
|
||||
void SetupTempVars();
|
||||
|
||||
enum class Type {
|
||||
FIXED,
|
||||
RANDOM,
|
||||
VARIABLE_WAIT,
|
||||
};
|
||||
Type _waitType = Type::FIXED;
|
||||
|
||||
// FIXED / RANDOM
|
||||
Duration _duration;
|
||||
Duration _duration2;
|
||||
|
||||
// VARIABLE_WAIT
|
||||
enum class Condition {
|
||||
EQUALS,
|
||||
DOES_NOT_EQUAL,
|
||||
IS_EMPTY,
|
||||
LESS_THAN,
|
||||
GREATER_THAN,
|
||||
};
|
||||
std::weak_ptr<Variable> _variable;
|
||||
Condition _condition = Condition::EQUALS;
|
||||
StringVariable _strValue = "";
|
||||
DoubleVariable _numValue = 0.0;
|
||||
RegexConfig _regex;
|
||||
bool _useTimeout = false;
|
||||
Duration _conditionTimeout;
|
||||
|
||||
private:
|
||||
bool PerformDurationWait();
|
||||
bool PerformVariableWait();
|
||||
bool ConditionIsMet() const;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
@@ -42,6 +71,7 @@ public:
|
||||
void UpdateEntryData();
|
||||
void SetupFixedDurationEdit();
|
||||
void SetupRandomDurationEdit();
|
||||
void SetupVariableWaitEdit();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
@@ -54,15 +84,36 @@ private slots:
|
||||
void DurationChanged(const Duration &value);
|
||||
void Duration2Changed(const Duration &value);
|
||||
void TypeChanged(int value);
|
||||
void VariableChanged(const QString &);
|
||||
void ConditionChanged(int);
|
||||
void StrValueChanged();
|
||||
void NumValueChanged(const NumberVariable<double> &);
|
||||
void RegexChanged(const RegexConfig &);
|
||||
void UseTimeoutChanged(int);
|
||||
void ConditionTimeoutChanged(const Duration &);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
private:
|
||||
void SetVariableWaitWidgetVisibility();
|
||||
|
||||
// Duration widgets (FIXED / RANDOM)
|
||||
DurationSelection *_duration;
|
||||
DurationSelection *_duration2;
|
||||
|
||||
// Variable condition widgets (VARIABLE_WAIT)
|
||||
VariableSelection *_variable;
|
||||
QComboBox *_variableCondition;
|
||||
VariableTextEdit *_strValue;
|
||||
VariableDoubleSpinBox *_numValue;
|
||||
RegexConfigWidget *_regex;
|
||||
QCheckBox *_useTimeout;
|
||||
DurationSelection *_conditionTimeout;
|
||||
|
||||
QComboBox *_waitType;
|
||||
QHBoxLayout *_mainLayout;
|
||||
QHBoxLayout *_timeoutLayout;
|
||||
|
||||
std::shared_ptr<MacroActionWait> _entryData;
|
||||
bool _loading = true;
|
||||
|
||||
@@ -15,6 +15,9 @@ bool MacroConditionProfile::_registered = MacroConditionFactory::Register(
|
||||
bool MacroConditionProfile::CheckCondition()
|
||||
{
|
||||
auto currentProfile = obs_frontend_get_current_profile();
|
||||
if (!currentProfile) {
|
||||
return false;
|
||||
}
|
||||
const bool match = _profile == currentProfile;
|
||||
bfree(currentProfile);
|
||||
return match;
|
||||
|
||||
@@ -15,7 +15,7 @@ if(NOT TARGET httplib)
|
||||
EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
if(OS_MACOS)
|
||||
if(OS_MACOS OR OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
@@ -25,6 +25,14 @@ if(NOT OPENSSL_FOUND)
|
||||
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
|
||||
return()
|
||||
endif()
|
||||
if(OS_WINDOWS AND NOT OPENSSL_CRYPTO_LIBRARY MATCHES "_static\\.lib$")
|
||||
message(
|
||||
WARNING
|
||||
"Static OpenSSL libraries (libcrypto_static.lib / libssl_static.lib) not found!\n"
|
||||
"HTTP support will be disabled to avoid DLL name collisions with other plugins.\n\n"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(ZLIB)
|
||||
if(NOT ZLIB_FOUND)
|
||||
@@ -65,29 +73,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
|
||||
"${OPENSSL_INCLUDE_DIR}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(OS_WINDOWS)
|
||||
# Couldn't really find a better way to install runtime dependencies for
|
||||
# Windows TODO: Clean this up at some point
|
||||
function(FIND_FILES_WITH_PATTERN result pattern dir)
|
||||
execute_process(
|
||||
COMMAND
|
||||
powershell -Command
|
||||
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
|
||||
"Select-Object -First 1 |"
|
||||
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
|
||||
OUTPUT_VARIABLE files
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set(${result}
|
||||
${files}
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
|
||||
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32 bcrypt)
|
||||
endif()
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
|
||||
@@ -3,12 +3,21 @@ project(advanced-scene-switcher-mqtt)
|
||||
|
||||
# --- Check paho.mqtt.cpp requirements ---
|
||||
|
||||
if(OS_MACOS)
|
||||
if(OS_MACOS OR OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
endif()
|
||||
find_package(OpenSSL)
|
||||
if(OS_WINDOWS
|
||||
AND OPENSSL_FOUND
|
||||
AND NOT OPENSSL_CRYPTO_LIBRARY MATCHES "_static\\.lib$")
|
||||
message(
|
||||
WARNING
|
||||
"Static OpenSSL libraries (libcrypto_static.lib / libssl_static.lib) not found!\n"
|
||||
"MQTT may fail at runtime due to OpenSSL DLL name collisions with other plugins.\n\n"
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(PahoMqttCpp)
|
||||
if(NOT PahoMqttCpp_FOUND)
|
||||
@@ -51,4 +60,7 @@ else()
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE PahoMqttCpp::paho-mqttpp3-static)
|
||||
endif()
|
||||
if(OS_WINDOWS)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32 bcrypt)
|
||||
endif()
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "macro-script-handler.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-action-script.hpp"
|
||||
#include "macro-condition-script.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <obs-properties.h>
|
||||
|
||||
@@ -255,39 +255,100 @@ static std::string preprocessScriptText(const std::string &text,
|
||||
const std::string &id)
|
||||
{
|
||||
const std::string footerPython =
|
||||
std::string("\n\n"
|
||||
"## AUTO GENERATED ##\n"
|
||||
"def script_load(settings):\n"
|
||||
" def run_wrapper(data):\n"
|
||||
" id = obs.calldata_string(data, \"id\")\n"
|
||||
" if id == \"") +
|
||||
R"(
|
||||
|
||||
## AUTO GENERATED ##
|
||||
def advss_get_variable_value(name):
|
||||
proc_handler = obs.obs_get_proc_handler()
|
||||
data = obs.calldata_create()
|
||||
obs.calldata_set_string(data, "name", name)
|
||||
obs.proc_handler_call(proc_handler, "advss_get_variable_value", data)
|
||||
success = obs.calldata_bool(data, "success")
|
||||
if success is False:
|
||||
obs.script_log(obs.LOG_WARNING, f'failed to get value for variable "{name}"')
|
||||
obs.calldata_destroy(data)
|
||||
return None
|
||||
value = obs.calldata_string(data, "value")
|
||||
obs.calldata_destroy(data)
|
||||
return value
|
||||
|
||||
def advss_set_variable_value(name, value):
|
||||
proc_handler = obs.obs_get_proc_handler()
|
||||
data = obs.calldata_create()
|
||||
obs.calldata_set_string(data, "name", name)
|
||||
obs.calldata_set_string(data, "value", value)
|
||||
obs.proc_handler_call(proc_handler, "advss_set_variable_value", data)
|
||||
success = obs.calldata_bool(data, "success")
|
||||
if success is False:
|
||||
obs.script_log(obs.LOG_WARNING, f'failed to set value for variable "{name}"')
|
||||
obs.calldata_destroy(data)
|
||||
return success
|
||||
|
||||
def script_load(settings):
|
||||
def run_wrapper(data):
|
||||
id = obs.calldata_string(data, "id")
|
||||
if id == ")" +
|
||||
id +
|
||||
"\":\n"
|
||||
" ret = run()\n"
|
||||
" obs.calldata_set_bool(data, \"result\", ret)\n"
|
||||
" sh = obs.obs_get_signal_handler()\n"
|
||||
" obs.signal_handler_connect(sh, \"" +
|
||||
signalName.data() + "\", run_wrapper)\n\n";
|
||||
R"(":
|
||||
ret = run()
|
||||
obs.calldata_set_bool(data, "result", ret)
|
||||
sh = obs.obs_get_signal_handler()
|
||||
obs.signal_handler_connect(sh, ")" +
|
||||
std::string(signalName) +
|
||||
R"(", run_wrapper)
|
||||
|
||||
)";
|
||||
|
||||
const std::string footerLUA =
|
||||
std::string(
|
||||
"\n\n"
|
||||
"-- AUTO GENERATED --\n"
|
||||
"function script_load(settings)\n"
|
||||
" local run_wrapper = (function(data)\n"
|
||||
" local id = obs.calldata_string(data, \"id\")\n"
|
||||
" if id == \"") +
|
||||
R"(
|
||||
|
||||
-- AUTO GENERATED --
|
||||
function advss_get_variable_value(name)
|
||||
local proc_handler = obs.obs_get_proc_handler()
|
||||
local data = obs.calldata_create()
|
||||
obs.calldata_set_string(data, "name", name)
|
||||
obs.proc_handler_call(proc_handler, "advss_get_variable_value", data)
|
||||
local success = obs.calldata_bool(data, "success")
|
||||
if success == false then
|
||||
obs.script_log(obs.LOG_WARNING, string.format("failed to get value for variable \"%s\"", name))
|
||||
obs.calldata_destroy(data)
|
||||
return nil
|
||||
end
|
||||
local value = obs.calldata_string(data, "value")
|
||||
obs.calldata_destroy(data)
|
||||
return value
|
||||
end
|
||||
|
||||
function advss_set_variable_value(name, value)
|
||||
local proc_handler = obs.obs_get_proc_handler()
|
||||
local data = obs.calldata_create()
|
||||
obs.calldata_set_string(data, "name", name)
|
||||
obs.calldata_set_string(data, "value", value)
|
||||
obs.proc_handler_call(proc_handler, "advss_set_variable_value", data)
|
||||
local success = obs.calldata_bool(data, "success")
|
||||
if success == false then
|
||||
obs.script_log(obs.LOG_WARNING, string.format("failed to set value for variable \"%s\"", name))
|
||||
end
|
||||
obs.calldata_destroy(data)
|
||||
return success
|
||||
end
|
||||
|
||||
function script_load(settings)
|
||||
local run_wrapper = (function(data)
|
||||
local id = obs.calldata_string(data, "id")
|
||||
if id == ")" +
|
||||
id +
|
||||
"\" then\n"
|
||||
" local ret = run()\n"
|
||||
" obs.calldata_set_bool(data, \"result\", ret)\n"
|
||||
" end\n"
|
||||
" end)\n"
|
||||
" local sh = obs.obs_get_signal_handler()\n"
|
||||
" obs.signal_handler_connect(sh, \"" +
|
||||
signalName.data() +
|
||||
"\" , run_wrapper)\n"
|
||||
"end\n";
|
||||
R"(" then
|
||||
local ret = run()
|
||||
obs.calldata_set_bool(data, "result", ret)
|
||||
end
|
||||
end)
|
||||
local sh = obs.obs_get_signal_handler()
|
||||
obs.signal_handler_connect(sh, ")" +
|
||||
std::string(signalName) +
|
||||
R"(" , run_wrapper)
|
||||
end
|
||||
)";
|
||||
|
||||
std::string scriptText =
|
||||
language == OBS_SCRIPT_LANG_PYTHON ? footerPython : footerLUA;
|
||||
|
||||
125
plugins/speech/CMakeLists.txt
Normal file
125
plugins/speech/CMakeLists.txt
Normal file
@@ -0,0 +1,125 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-speech)
|
||||
|
||||
# --- Check requirements ---
|
||||
|
||||
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
|
||||
set(WHISPER_DIR "${ADVSS_SOURCE_DIR}/deps/whisper.cpp")
|
||||
if(NOT EXISTS "${WHISPER_DIR}/CMakeLists.txt")
|
||||
message(WARNING "whisper.cpp directory \"${WHISPER_DIR}\" not found!\n"
|
||||
"Speech condition will be disabled!\n\n"
|
||||
"Clone whisper.cpp into: ${WHISPER_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(WHISPER_BUILD_TESTS
|
||||
OFF
|
||||
CACHE BOOL "" FORCE)
|
||||
set(WHISPER_BUILD_EXAMPLES
|
||||
OFF
|
||||
CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS
|
||||
OFF
|
||||
CACHE BOOL "" FORCE)
|
||||
|
||||
# Vulkan GPU backend. Auto-enabled when VULKAN_SDK is set (e.g. in CI).
|
||||
option(ADVSS_SPEECH_VULKAN "Use Vulkan GPU backend for speech recognition" OFF)
|
||||
|
||||
if(NOT ADVSS_SPEECH_VULKAN AND DEFINED ENV{VULKAN_SDK})
|
||||
set(ADVSS_SPEECH_VULKAN ON)
|
||||
endif()
|
||||
|
||||
if(ADVSS_SPEECH_VULKAN)
|
||||
set(GGML_VULKAN
|
||||
ON
|
||||
CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
|
||||
# Suppress warnings-as-errors on all targets in a whisper.cpp source subtree.
|
||||
function(_advss_whisper_suppress_werror dir)
|
||||
get_property(
|
||||
_subdirs
|
||||
DIRECTORY "${dir}"
|
||||
PROPERTY SUBDIRECTORIES)
|
||||
foreach(_sub IN LISTS _subdirs)
|
||||
_advss_whisper_suppress_werror("${_sub}")
|
||||
endforeach()
|
||||
get_property(
|
||||
_targets
|
||||
DIRECTORY "${dir}"
|
||||
PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach(_target IN LISTS _targets)
|
||||
get_target_property(_type ${_target} TYPE)
|
||||
if(_type STREQUAL "INTERFACE_LIBRARY" OR _type STREQUAL "UTILITY")
|
||||
continue()
|
||||
endif()
|
||||
get_target_property(_opts ${_target} COMPILE_OPTIONS)
|
||||
if(_opts)
|
||||
list(FILTER _opts EXCLUDE REGEX "^-Werror")
|
||||
list(REMOVE_ITEM _opts /WX)
|
||||
set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}")
|
||||
endif()
|
||||
target_compile_options(
|
||||
${_target}
|
||||
PRIVATE $<$<C_COMPILER_ID:GNU,Clang,AppleClang>:-Wno-error>
|
||||
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wno-error>
|
||||
$<$<C_COMPILER_ID:MSVC>:/WX-> $<$<CXX_COMPILER_ID:MSVC>:/WX->)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
set(_advss_saved_werror ${CMAKE_COMPILE_WARNING_AS_ERROR})
|
||||
set(_advss_saved_pic ${CMAKE_POSITION_INDEPENDENT_CODE})
|
||||
set(CMAKE_COMPILE_WARNING_AS_ERROR OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if(NOT TARGET whisper)
|
||||
add_subdirectory("${WHISPER_DIR}" "${CMAKE_BINARY_DIR}/whisper.cpp"
|
||||
EXCLUDE_FROM_ALL)
|
||||
_advss_whisper_suppress_werror("${WHISPER_DIR}")
|
||||
# ggml-metal's .m files use manual retain/release; disable ARC on that target.
|
||||
if(TARGET ggml-metal)
|
||||
set_target_properties(ggml-metal
|
||||
PROPERTIES XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC NO)
|
||||
target_compile_options(ggml-metal
|
||||
PRIVATE $<$<COMPILE_LANGUAGE:OBJC>:-fno-objc-arc>)
|
||||
endif()
|
||||
endif()
|
||||
set(CMAKE_COMPILE_WARNING_AS_ERROR ${_advss_saved_werror})
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ${_advss_saved_pic})
|
||||
|
||||
# ggml-vulkan finds SPIRV-Headers but does not link it.
|
||||
if(ADVSS_SPEECH_VULKAN
|
||||
AND TARGET ggml-vulkan
|
||||
AND TARGET SPIRV-Headers::SPIRV-Headers)
|
||||
target_link_libraries(ggml-vulkan PRIVATE SPIRV-Headers::SPIRV-Headers)
|
||||
endif()
|
||||
|
||||
# OBS deps ship older Vulkan headers; put the SDK headers first so ggml-vulkan
|
||||
# sees the full API.
|
||||
if(ADVSS_SPEECH_VULKAN
|
||||
AND TARGET ggml-vulkan
|
||||
AND DEFINED ENV{VULKAN_SDK})
|
||||
if(WIN32)
|
||||
set(_vk_sdk_include "$ENV{VULKAN_SDK}/Include")
|
||||
else()
|
||||
set(_vk_sdk_include "$ENV{VULKAN_SDK}/include")
|
||||
endif()
|
||||
if(EXISTS "${_vk_sdk_include}")
|
||||
target_include_directories(ggml-vulkan BEFORE PRIVATE "${_vk_sdk_include}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME} PRIVATE macro-condition-speech.cpp macro-condition-speech.hpp
|
||||
speech-recognizer.cpp speech-recognizer.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${WHISPER_DIR}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE whisper)
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
660
plugins/speech/macro-condition-speech.cpp
Normal file
660
plugins/speech/macro-condition-speech.cpp
Normal file
@@ -0,0 +1,660 @@
|
||||
#include "macro-condition-speech.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
|
||||
#include <obs-module.h>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static int defaultNThreads()
|
||||
{
|
||||
return (int)std::min(
|
||||
4u, std::max(1u, std::thread::hardware_concurrency() / 4));
|
||||
}
|
||||
|
||||
static std::string getDefaultModelPath()
|
||||
{
|
||||
return std::string(obs_get_module_data_path(obs_current_module())) +
|
||||
"/res/speech/ggml-tiny-q8_0.bin";
|
||||
}
|
||||
|
||||
static QStringList getAudioSourcesList()
|
||||
{
|
||||
auto sources = GetAudioSourceNames();
|
||||
sources.sort();
|
||||
return sources;
|
||||
}
|
||||
|
||||
const std::string MacroConditionSpeech::id = "speech";
|
||||
|
||||
bool MacroConditionSpeech::_registered = MacroConditionFactory::Register(
|
||||
MacroConditionSpeech::id,
|
||||
{MacroConditionSpeech::Create, MacroConditionSpeechEdit::Create,
|
||||
"AdvSceneSwitcher.condition.speech"});
|
||||
|
||||
MacroConditionSpeech::MacroConditionSpeech(Macro *m)
|
||||
: MacroCondition(m),
|
||||
_modelPath(getDefaultModelPath()),
|
||||
_nThreads(defaultNThreads()),
|
||||
_language("auto"),
|
||||
_messageBuffer(_recognizer.RegisterClient())
|
||||
{
|
||||
_recognizer.SetNThreads(defaultNThreads());
|
||||
_recognizer.SetLanguage("auto");
|
||||
_recognizer.SetTranslate(false);
|
||||
_recognizer.SetVadEnergyThreshold(1e-4f);
|
||||
_recognizer.SetSuppressNonSpeechTokens(true);
|
||||
_recognizer.SetNoContext(true);
|
||||
}
|
||||
|
||||
MacroConditionSpeech::~MacroConditionSpeech()
|
||||
{
|
||||
if (_rebuildThread.joinable()) {
|
||||
_rebuildThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetCondition(Condition c)
|
||||
{
|
||||
_condition = c;
|
||||
SetupTempVars();
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetModelPath(const std::string &path)
|
||||
{
|
||||
_modelPath = path;
|
||||
RebuildRecognizer();
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetBufferDuration(const DoubleVariable &value)
|
||||
{
|
||||
_bufferDuration = value;
|
||||
_recognizer.SetBufferDuration((double)_bufferDuration);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetNThreads(const IntVariable &value)
|
||||
{
|
||||
_nThreads = value;
|
||||
_recognizer.SetNThreads((int)_nThreads);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetLanguage(const std::string &lang)
|
||||
{
|
||||
_language = lang;
|
||||
_recognizer.SetLanguage(lang);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetTranslate(bool translate)
|
||||
{
|
||||
_translate = translate;
|
||||
_recognizer.SetTranslate(translate);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetVadEnergyThreshold(const DoubleVariable &value)
|
||||
{
|
||||
_vadEnergyThreshold = value;
|
||||
_recognizer.SetVadEnergyThreshold((float)(double)_vadEnergyThreshold);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetSuppressNonSpeechTokens(bool suppress)
|
||||
{
|
||||
_suppressNonSpeechTokens = suppress;
|
||||
_recognizer.SetSuppressNonSpeechTokens(suppress);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetNoContext(bool noContext)
|
||||
{
|
||||
_noContext = noContext;
|
||||
_recognizer.SetNoContext(noContext);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetListenWhenMuted(bool listen)
|
||||
{
|
||||
_listenWhenMuted = listen;
|
||||
_recognizer.SetListenWhenMuted(listen);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetUseGpu(bool useGpu)
|
||||
{
|
||||
_useGpu = useGpu;
|
||||
_recognizer.SetUseGpu(useGpu);
|
||||
RebuildRecognizer();
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::RebuildRecognizer()
|
||||
{
|
||||
_recognizer.StopCapture();
|
||||
|
||||
if (_rebuildThread.joinable()) {
|
||||
_rebuildThread.join();
|
||||
}
|
||||
|
||||
const std::string path = _modelPath;
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
OBSWeakSource weakSource = _source.GetSource();
|
||||
_messageBuffer = _recognizer.RegisterClient();
|
||||
|
||||
_rebuildThread = std::thread([this, path, weakSource]() {
|
||||
if (!_recognizer.LoadModel(path)) {
|
||||
return;
|
||||
}
|
||||
OBSSource source = OBSGetStrongRef(weakSource);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
_recognizer.StartCapture(source);
|
||||
});
|
||||
}
|
||||
|
||||
bool MacroConditionSpeech::CheckCondition()
|
||||
{
|
||||
std::string lastTranscript;
|
||||
bool anyReceived = false;
|
||||
|
||||
while (!_messageBuffer->Empty()) {
|
||||
auto msg = _messageBuffer->ConsumeMessage();
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
lastTranscript = *msg;
|
||||
anyReceived = true;
|
||||
}
|
||||
|
||||
if (anyReceived) {
|
||||
SetTempVarValue("speech", lastTranscript);
|
||||
}
|
||||
|
||||
switch (_condition) {
|
||||
case Condition::ANY:
|
||||
if (anyReceived) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Condition::CONTAINS: {
|
||||
if (!anyReceived) {
|
||||
return false;
|
||||
}
|
||||
const std::string phrase = _phrase;
|
||||
const QRegularExpression re(
|
||||
"\\b" +
|
||||
QRegularExpression::escape(
|
||||
QString::fromStdString(phrase)) +
|
||||
"\\b",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
if (re.match(QString::fromStdString(lastTranscript)).hasMatch()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case Condition::MATCHES:
|
||||
if (!anyReceived) {
|
||||
return false;
|
||||
}
|
||||
if (_regex.Enabled() &&
|
||||
_regex.Matches(lastTranscript, _phrase)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetupTempVars()
|
||||
{
|
||||
MacroCondition::SetupTempVars();
|
||||
AddTempvar(
|
||||
"speech",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.speech.speech"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.speech.speech.description"));
|
||||
}
|
||||
|
||||
std::string MacroConditionSpeech::GetShortDesc() const
|
||||
{
|
||||
return _source.ToString();
|
||||
}
|
||||
|
||||
bool MacroConditionSpeech::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroCondition::Save(obj);
|
||||
_source.Save(obj, "source");
|
||||
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
|
||||
_phrase.Save(obj, "phrase");
|
||||
_regex.Save(obj);
|
||||
_modelPath.Save(obj, "modelPath");
|
||||
_bufferDuration.Save(obj, "bufferDuration");
|
||||
_nThreads.Save(obj, "nThreads");
|
||||
_language.Save(obj, "language");
|
||||
obs_data_set_bool(obj, "translate", _translate);
|
||||
_vadEnergyThreshold.Save(obj, "vadEnergyThreshold");
|
||||
obs_data_set_bool(obj, "suppressNonSpeechTokens",
|
||||
_suppressNonSpeechTokens);
|
||||
obs_data_set_bool(obj, "noContext", _noContext);
|
||||
obs_data_set_bool(obj, "listenWhenMuted", _listenWhenMuted);
|
||||
obs_data_set_bool(obj, "useGpu", _useGpu);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroConditionSpeech::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroCondition::Load(obj);
|
||||
_source.Load(obj, "source");
|
||||
SetCondition(
|
||||
static_cast<Condition>(obs_data_get_int(obj, "condition")));
|
||||
_phrase.Load(obj, "phrase");
|
||||
_regex.Load(obj);
|
||||
_modelPath.Load(obj, "modelPath");
|
||||
_bufferDuration.Load(obj, "bufferDuration");
|
||||
_nThreads.Load(obj, "nThreads");
|
||||
_language.Load(obj, "language");
|
||||
_translate = obs_data_get_bool(obj, "translate");
|
||||
_vadEnergyThreshold.Load(obj, "vadEnergyThreshold");
|
||||
_suppressNonSpeechTokens =
|
||||
obs_data_get_bool(obj, "suppressNonSpeechTokens");
|
||||
_noContext = obs_data_get_bool(obj, "noContext");
|
||||
_listenWhenMuted = obs_data_get_bool(obj, "listenWhenMuted");
|
||||
_useGpu = obs_data_get_bool(obj, "useGpu");
|
||||
_recognizer.SetNThreads((int)_nThreads);
|
||||
_recognizer.SetLanguage(std::string(_language));
|
||||
_recognizer.SetTranslate(_translate);
|
||||
_recognizer.SetVadEnergyThreshold((float)(double)_vadEnergyThreshold);
|
||||
_recognizer.SetSuppressNonSpeechTokens(_suppressNonSpeechTokens);
|
||||
_recognizer.SetNoContext(_noContext);
|
||||
_recognizer.SetListenWhenMuted(_listenWhenMuted);
|
||||
_recognizer.SetUseGpu(_useGpu);
|
||||
RebuildRecognizer();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void populateConditionSelection(QComboBox *list)
|
||||
{
|
||||
static const std::map<MacroConditionSpeech::Condition, std::string>
|
||||
conditionTypes = {
|
||||
{MacroConditionSpeech::Condition::ANY,
|
||||
"AdvSceneSwitcher.condition.speech.condition.any"},
|
||||
{MacroConditionSpeech::Condition::CONTAINS,
|
||||
"AdvSceneSwitcher.condition.speech.condition.contains"},
|
||||
{MacroConditionSpeech::Condition::MATCHES,
|
||||
"AdvSceneSwitcher.condition.speech.condition.matches"},
|
||||
};
|
||||
|
||||
for (const auto &[cond, name] : conditionTypes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(cond));
|
||||
}
|
||||
}
|
||||
|
||||
MacroConditionSpeechEdit::MacroConditionSpeechEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionSpeech> entryData)
|
||||
: QWidget(parent),
|
||||
_source(new SourceSelectionWidget(this, getAudioSourcesList, true)),
|
||||
_conditions(new QComboBox(this)),
|
||||
_phrase(new VariableLineEdit(this)),
|
||||
_regex(new RegexConfigWidget(parent)),
|
||||
_modelPath(new FileSelection(
|
||||
FileSelection::Type::READ, this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.browse.title"))),
|
||||
_modelHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.model.help"),
|
||||
this)),
|
||||
_bufferDuration(new VariableDoubleSpinBox(this)),
|
||||
_bufferHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.buffer.help"),
|
||||
this)),
|
||||
_advancedSection(new Section(300, this)),
|
||||
_nThreads(new VariableSpinBox(this)),
|
||||
_language(new VariableLineEdit(this)),
|
||||
_languageHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.language.help"),
|
||||
this)),
|
||||
_translate(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.translate"),
|
||||
this)),
|
||||
_translateHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.translate.help"),
|
||||
this)),
|
||||
_vadEnergyThreshold(new VariableDoubleSpinBox(this)),
|
||||
_vadHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.vad.help"),
|
||||
this)),
|
||||
_suppressNonSpeechTokens(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.suppress"),
|
||||
this)),
|
||||
_suppressHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.suppress.help"),
|
||||
this)),
|
||||
_noContext(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.noContext"),
|
||||
this)),
|
||||
_noContextHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.noContext.help"),
|
||||
this)),
|
||||
_listenWhenMuted(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.listenWhenMuted"),
|
||||
this)),
|
||||
_useGpu(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.useGpu"),
|
||||
this))
|
||||
{
|
||||
populateConditionSelection(_conditions);
|
||||
|
||||
_bufferDuration->setMinimum(1.0);
|
||||
_bufferDuration->setMaximum(30.0);
|
||||
_bufferDuration->SpinBox()->setSingleStep(0.5);
|
||||
_bufferDuration->setSuffix(" s");
|
||||
|
||||
_nThreads->setMinimum(1);
|
||||
_nThreads->setMaximum(32);
|
||||
|
||||
_vadEnergyThreshold->setMinimum(0.0);
|
||||
_vadEnergyThreshold->setMaximum(1.0);
|
||||
_vadEnergyThreshold->SpinBox()->setSingleStep(1e-5);
|
||||
_vadEnergyThreshold->SpinBox()->setDecimals(6);
|
||||
|
||||
QWidget::connect(_source,
|
||||
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||
SLOT(SourceChanged(const SourceSelection &)));
|
||||
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(ConditionChanged(int)));
|
||||
QWidget::connect(_phrase, SIGNAL(editingFinished()), this,
|
||||
SLOT(PhraseChanged()));
|
||||
QWidget::connect(_regex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(RegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_modelPath, SIGNAL(PathChanged(const QString &)), this,
|
||||
SLOT(ModelPathChanged(const QString &)));
|
||||
QWidget::connect(
|
||||
_bufferDuration,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(BufferDurationChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(
|
||||
_nThreads,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
|
||||
this, SLOT(NThreadsChanged(const NumberVariable<int> &)));
|
||||
QWidget::connect(_language, SIGNAL(editingFinished()), this,
|
||||
SLOT(LanguageChanged()));
|
||||
QWidget::connect(_translate, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(TranslateChanged(int)));
|
||||
QWidget::connect(
|
||||
_vadEnergyThreshold,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(VadEnergyThresholdChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(_suppressNonSpeechTokens, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(SuppressNonSpeechTokensChanged(int)));
|
||||
QWidget::connect(_noContext, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(NoContextChanged(int)));
|
||||
QWidget::connect(_listenWhenMuted, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(ListenWhenMutedChanged(int)));
|
||||
QWidget::connect(_useGpu, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseGpuChanged(int)));
|
||||
|
||||
_condSourceLayout = new QHBoxLayout;
|
||||
|
||||
_phraseLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.phrase"),
|
||||
_phraseLayout,
|
||||
{{"{{phrase}}", _phrase}, {"{{regex}}", _regex}}, false);
|
||||
|
||||
auto *modelLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.model"),
|
||||
modelLayout,
|
||||
{{"{{modelPath}}", _modelPath}, {"{{help}}", _modelHelp}},
|
||||
false);
|
||||
|
||||
auto *bufferLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.buffer"),
|
||||
bufferLayout,
|
||||
{{"{{bufferDuration}}", _bufferDuration},
|
||||
{"{{help}}", _bufferHelp}});
|
||||
|
||||
auto *threadsLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.threads"),
|
||||
threadsLayout, {{"{{threads}}", _nThreads}});
|
||||
|
||||
auto *languageLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.language"),
|
||||
languageLayout,
|
||||
{{"{{language}}", _language}, {"{{help}}", _languageHelp}});
|
||||
|
||||
auto *translateLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.translate"),
|
||||
translateLayout,
|
||||
{{"{{translate}}", _translate}, {"{{help}}", _translateHelp}});
|
||||
|
||||
auto *vadLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.vad"),
|
||||
vadLayout,
|
||||
{{"{{vad}}", _vadEnergyThreshold}, {"{{help}}", _vadHelp}});
|
||||
|
||||
auto *suppressLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.suppress"),
|
||||
suppressLayout,
|
||||
{{"{{suppress}}", _suppressNonSpeechTokens},
|
||||
{"{{help}}", _suppressHelp}});
|
||||
|
||||
auto *noContextLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.noContext"),
|
||||
noContextLayout,
|
||||
{{"{{noContext}}", _noContext}, {"{{help}}", _noContextHelp}});
|
||||
|
||||
auto *advancedContent = new QWidget(this);
|
||||
auto *advancedLayout = new QVBoxLayout;
|
||||
advancedLayout->addLayout(threadsLayout);
|
||||
advancedLayout->addLayout(languageLayout);
|
||||
advancedLayout->addLayout(translateLayout);
|
||||
advancedLayout->addLayout(vadLayout);
|
||||
advancedLayout->addLayout(suppressLayout);
|
||||
advancedLayout->addLayout(noContextLayout);
|
||||
advancedLayout->addWidget(_listenWhenMuted);
|
||||
advancedLayout->addWidget(_useGpu);
|
||||
advancedContent->setLayout(advancedLayout);
|
||||
|
||||
_advancedSection->AddHeaderWidget(new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.condition.speech.advanced"),
|
||||
this));
|
||||
_advancedSection->SetContent(advancedContent, true);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(_condSourceLayout);
|
||||
mainLayout->addLayout(_phraseLayout);
|
||||
mainLayout->addLayout(modelLayout);
|
||||
mainLayout->addLayout(bufferLayout);
|
||||
mainLayout->addWidget(_advancedSection);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_source->SetSource(_entryData->_source);
|
||||
_conditions->setCurrentIndex(
|
||||
static_cast<int>(_entryData->GetCondition()));
|
||||
_phrase->setText(QString::fromStdString(_entryData->_phrase));
|
||||
_regex->SetRegexConfig(_entryData->_regex);
|
||||
_modelPath->SetPath(_entryData->GetModelPath());
|
||||
_bufferDuration->SetValue(_entryData->GetBufferDuration());
|
||||
_nThreads->SetValue(_entryData->GetNThreads());
|
||||
_language->setText(QString::fromStdString(_entryData->GetLanguage()));
|
||||
_translate->setChecked(_entryData->GetTranslate());
|
||||
_vadEnergyThreshold->SetValue(_entryData->GetVadEnergyThreshold());
|
||||
_suppressNonSpeechTokens->setChecked(
|
||||
_entryData->GetSuppressNonSpeechTokens());
|
||||
_noContext->setChecked(_entryData->GetNoContext());
|
||||
_listenWhenMuted->setChecked(_entryData->GetListenWhenMuted());
|
||||
_useGpu->setChecked(_entryData->GetUseGpu());
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::SourceChanged(const SourceSelection &source)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_source = source;
|
||||
_entryData->RebuildRecognizer();
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::ConditionChanged(int idx)
|
||||
{
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetCondition(
|
||||
static_cast<MacroConditionSpeech::Condition>(
|
||||
_conditions->itemData(idx).toInt()));
|
||||
}
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::PhraseChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_phrase = _phrase->text().toStdString();
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::RegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_regex = conf;
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::ModelPathChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetModelPath(path.toStdString());
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::BufferDurationChanged(
|
||||
const NumberVariable<double> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetBufferDuration(value);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::NThreadsChanged(const NumberVariable<int> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetNThreads(value);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::LanguageChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetLanguage(_language->text().toStdString());
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::TranslateChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetTranslate(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::VadEnergyThresholdChanged(
|
||||
const NumberVariable<double> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetVadEnergyThreshold(value);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::SuppressNonSpeechTokensChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetSuppressNonSpeechTokens(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::NoContextChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetNoContext(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::ListenWhenMutedChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetListenWhenMuted(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::UseGpuChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetUseGpu(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::SetWidgetVisibility()
|
||||
{
|
||||
const auto condition = _entryData->GetCondition();
|
||||
const bool hasPhrase = condition !=
|
||||
MacroConditionSpeech::Condition::ANY;
|
||||
|
||||
_condSourceLayout->removeWidget(_conditions);
|
||||
_condSourceLayout->removeWidget(_source);
|
||||
ClearLayout(_condSourceLayout);
|
||||
const char *layoutKey = "AdvSceneSwitcher.condition.speech.layout.any";
|
||||
if (condition == MacroConditionSpeech::Condition::CONTAINS) {
|
||||
layoutKey = "AdvSceneSwitcher.condition.speech.layout.contains";
|
||||
} else if (condition == MacroConditionSpeech::Condition::MATCHES) {
|
||||
layoutKey = "AdvSceneSwitcher.condition.speech.layout.matches";
|
||||
}
|
||||
PlaceWidgets(obs_module_text(layoutKey), _condSourceLayout,
|
||||
{{"{{conditions}}", _conditions},
|
||||
{"{{source}}", _source}});
|
||||
|
||||
SetLayoutVisible(_phraseLayout, hasPhrase);
|
||||
_regex->setVisible(condition ==
|
||||
MacroConditionSpeech::Condition::MATCHES);
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
182
plugins/speech/macro-condition-speech.hpp
Normal file
182
plugins/speech/macro-condition-speech.hpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#pragma once
|
||||
#include "file-selection.hpp"
|
||||
#include "help-icon.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "regex-config.hpp"
|
||||
#include "section.hpp"
|
||||
#include "source-selection.hpp"
|
||||
#include "speech-recognizer.hpp"
|
||||
#include "variable-line-edit.hpp"
|
||||
#include "variable-number.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
#include "variable-string.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include <thread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroConditionSpeech : public MacroCondition {
|
||||
public:
|
||||
MacroConditionSpeech(Macro *m);
|
||||
~MacroConditionSpeech();
|
||||
|
||||
bool CheckCondition() override;
|
||||
bool Save(obs_data_t *obj) const override;
|
||||
bool Load(obs_data_t *obj) override;
|
||||
std::string GetShortDesc() const override;
|
||||
std::string GetId() const override { return id; }
|
||||
|
||||
static std::shared_ptr<MacroCondition> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroConditionSpeech>(m);
|
||||
}
|
||||
|
||||
enum class Condition {
|
||||
ANY,
|
||||
CONTAINS, // Just a more user friendly variant of "matches"
|
||||
MATCHES,
|
||||
};
|
||||
|
||||
void SetCondition(Condition c);
|
||||
Condition GetCondition() const { return _condition; }
|
||||
|
||||
void SetModelPath(const std::string &path);
|
||||
const StringVariable &GetModelPath() const { return _modelPath; }
|
||||
|
||||
void SetBufferDuration(const DoubleVariable &value);
|
||||
DoubleVariable GetBufferDuration() const { return _bufferDuration; }
|
||||
|
||||
void SetNThreads(const IntVariable &value);
|
||||
IntVariable GetNThreads() const { return _nThreads; }
|
||||
|
||||
void SetLanguage(const std::string &lang);
|
||||
const StringVariable &GetLanguage() const { return _language; }
|
||||
|
||||
void SetTranslate(bool translate);
|
||||
bool GetTranslate() const { return _translate; }
|
||||
|
||||
void SetVadEnergyThreshold(const DoubleVariable &value);
|
||||
DoubleVariable GetVadEnergyThreshold() const
|
||||
{
|
||||
return _vadEnergyThreshold;
|
||||
}
|
||||
|
||||
void SetSuppressNonSpeechTokens(bool suppress);
|
||||
bool GetSuppressNonSpeechTokens() const
|
||||
{
|
||||
return _suppressNonSpeechTokens;
|
||||
}
|
||||
|
||||
void SetNoContext(bool noContext);
|
||||
bool GetNoContext() const { return _noContext; }
|
||||
|
||||
void SetListenWhenMuted(bool listen);
|
||||
bool GetListenWhenMuted() const { return _listenWhenMuted; }
|
||||
|
||||
void SetUseGpu(bool useGpu);
|
||||
bool GetUseGpu() const { return _useGpu; }
|
||||
|
||||
SourceSelection _source;
|
||||
StringVariable _phrase = "";
|
||||
RegexConfig _regex;
|
||||
|
||||
void RebuildRecognizer();
|
||||
|
||||
private:
|
||||
void SetupTempVars() override;
|
||||
|
||||
Condition _condition = Condition::ANY;
|
||||
StringVariable _modelPath;
|
||||
DoubleVariable _bufferDuration = 5.0;
|
||||
IntVariable _nThreads;
|
||||
StringVariable _language;
|
||||
bool _translate = false;
|
||||
DoubleVariable _vadEnergyThreshold = 1e-4;
|
||||
bool _suppressNonSpeechTokens = true;
|
||||
bool _noContext = true;
|
||||
bool _listenWhenMuted = false;
|
||||
bool _useGpu = true;
|
||||
|
||||
SpeechRecognizer _recognizer;
|
||||
std::shared_ptr<MessageBuffer<std::string>> _messageBuffer;
|
||||
std::thread _rebuildThread;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroConditionSpeechEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroConditionSpeechEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroConditionSpeech> entryData = nullptr);
|
||||
void UpdateEntryData();
|
||||
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroCondition> cond)
|
||||
{
|
||||
return new MacroConditionSpeechEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroConditionSpeech>(cond));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void SourceChanged(const SourceSelection &);
|
||||
void ConditionChanged(int);
|
||||
void PhraseChanged();
|
||||
void RegexChanged(const RegexConfig &);
|
||||
void ModelPathChanged(const QString &);
|
||||
void BufferDurationChanged(const NumberVariable<double> &);
|
||||
void NThreadsChanged(const NumberVariable<int> &);
|
||||
void LanguageChanged();
|
||||
void TranslateChanged(int);
|
||||
void VadEnergyThresholdChanged(const NumberVariable<double> &);
|
||||
void SuppressNonSpeechTokensChanged(int);
|
||||
void NoContextChanged(int);
|
||||
void ListenWhenMutedChanged(int);
|
||||
void UseGpuChanged(int);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
|
||||
SourceSelectionWidget *_source;
|
||||
QComboBox *_conditions;
|
||||
VariableLineEdit *_phrase;
|
||||
RegexConfigWidget *_regex;
|
||||
FileSelection *_modelPath;
|
||||
HelpIcon *_modelHelp;
|
||||
VariableDoubleSpinBox *_bufferDuration;
|
||||
HelpIcon *_bufferHelp;
|
||||
Section *_advancedSection;
|
||||
VariableSpinBox *_nThreads;
|
||||
VariableLineEdit *_language;
|
||||
HelpIcon *_languageHelp;
|
||||
QCheckBox *_translate;
|
||||
HelpIcon *_translateHelp;
|
||||
VariableDoubleSpinBox *_vadEnergyThreshold;
|
||||
HelpIcon *_vadHelp;
|
||||
QCheckBox *_suppressNonSpeechTokens;
|
||||
HelpIcon *_suppressHelp;
|
||||
QCheckBox *_noContext;
|
||||
HelpIcon *_noContextHelp;
|
||||
QCheckBox *_listenWhenMuted;
|
||||
QCheckBox *_useGpu;
|
||||
|
||||
QHBoxLayout *_condSourceLayout;
|
||||
QHBoxLayout *_phraseLayout;
|
||||
|
||||
std::shared_ptr<MacroConditionSpeech> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
402
plugins/speech/speech-recognizer.cpp
Normal file
402
plugins/speech/speech-recognizer.cpp
Normal file
@@ -0,0 +1,402 @@
|
||||
#include "speech-recognizer.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <obs.h>
|
||||
#include <media-io/audio-resampler.h>
|
||||
#include <util/platform.h>
|
||||
|
||||
#include <whisper.h>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Ignore configured log level until after loading is complete to ensure we
|
||||
// catch the initial whisper configuration logs
|
||||
static bool ignoreLogFilter = true;
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddFinishedLoadingStep([]() { ignoreLogFilter = false; });
|
||||
return true;
|
||||
}
|
||||
const bool _ = setup();
|
||||
|
||||
static void whisperLogCallback(ggml_log_level level, const char *text, void *)
|
||||
{
|
||||
if (!text || *text == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int obsLevel = LOG_INFO;
|
||||
if (level == GGML_LOG_LEVEL_WARN) {
|
||||
obsLevel = LOG_WARNING;
|
||||
} else if (level == GGML_LOG_LEVEL_ERROR) {
|
||||
obsLevel = LOG_ERROR;
|
||||
}
|
||||
|
||||
std::string msg(text);
|
||||
if (!msg.empty() && msg.back() == '\n') {
|
||||
msg.pop_back();
|
||||
}
|
||||
|
||||
if (msg.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreLogFilter) {
|
||||
blog(obsLevel, "[speech] %s", msg.c_str());
|
||||
} else {
|
||||
vblog(obsLevel, "[speech] %s", msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr int whisperSampleRate = 16000;
|
||||
|
||||
// How often to evaluate VAD and potentially trigger inference
|
||||
static constexpr double stepDurationSeconds = 1.0;
|
||||
|
||||
// Audio kept from the previous inference run to provide word-boundary context
|
||||
// for the next run.
|
||||
// I guess repeating something is better than potentially missing stuff.
|
||||
static constexpr double keepDurationSeconds = 0.2;
|
||||
|
||||
SpeechRecognizer::SpeechRecognizer()
|
||||
{
|
||||
_inferenceThread = std::thread(&SpeechRecognizer::InferenceLoop, this);
|
||||
}
|
||||
|
||||
SpeechRecognizer::~SpeechRecognizer()
|
||||
{
|
||||
StopCapture();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_inferenceMutex);
|
||||
_stopThread = true;
|
||||
_bufferReady = true;
|
||||
}
|
||||
_inferenceCV.notify_one();
|
||||
if (_inferenceThread.joinable()) {
|
||||
_inferenceThread.join();
|
||||
}
|
||||
|
||||
if (_ctx) {
|
||||
whisper_free(_ctx);
|
||||
}
|
||||
if (_resampler) {
|
||||
audio_resampler_destroy(
|
||||
static_cast<audio_resampler_t *>(_resampler));
|
||||
}
|
||||
}
|
||||
|
||||
bool SpeechRecognizer::LoadModel(const std::string &modelPath)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_ctxMutex);
|
||||
|
||||
if (_ctx) {
|
||||
whisper_free(_ctx);
|
||||
_ctx = nullptr;
|
||||
}
|
||||
|
||||
whisper_log_set(whisperLogCallback, nullptr);
|
||||
|
||||
whisper_context_params cparams = whisper_context_default_params();
|
||||
cparams.use_gpu = _useGpu;
|
||||
_ctx = whisper_init_from_file_with_params(modelPath.c_str(), cparams);
|
||||
if (!_ctx) {
|
||||
blog(LOG_WARNING, "failed to load whisper model: %s",
|
||||
modelPath.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpeechRecognizer::StartCapture(obs_source_t *source)
|
||||
{
|
||||
StopCapture();
|
||||
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const audio_t *audio = obs_get_audio();
|
||||
if (!audio) {
|
||||
return false;
|
||||
}
|
||||
const struct audio_output_info *aoi = audio_output_get_info(audio);
|
||||
_sourceSampleRate = (int)aoi->samples_per_sec;
|
||||
_sourceChannelCount = (int)get_audio_channels(aoi->speakers);
|
||||
|
||||
if (_resampler) {
|
||||
audio_resampler_destroy(
|
||||
static_cast<audio_resampler_t *>(_resampler));
|
||||
_resampler = nullptr;
|
||||
}
|
||||
|
||||
struct resample_info srcInfo = {};
|
||||
srcInfo.samples_per_sec = (uint32_t)_sourceSampleRate;
|
||||
srcInfo.format = AUDIO_FORMAT_FLOAT_PLANAR;
|
||||
srcInfo.speakers = aoi->speakers;
|
||||
|
||||
struct resample_info dstInfo = {};
|
||||
dstInfo.samples_per_sec = whisperSampleRate;
|
||||
dstInfo.format = AUDIO_FORMAT_FLOAT;
|
||||
dstInfo.speakers = SPEAKERS_MONO;
|
||||
|
||||
_resampler = audio_resampler_create(&dstInfo, &srcInfo);
|
||||
if (!_resampler) {
|
||||
blog(LOG_WARNING,
|
||||
"failed to create audio resampler for speech condition");
|
||||
return false;
|
||||
}
|
||||
|
||||
_captureSource = obs_source_get_weak_source(source);
|
||||
obs_source_add_audio_capture_callback(source, AudioCaptureCallback,
|
||||
this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::StopCapture()
|
||||
{
|
||||
OBSSource source = OBSGetStrongRef(_captureSource);
|
||||
if (source) {
|
||||
obs_source_remove_audio_capture_callback(
|
||||
source, AudioCaptureCallback, this);
|
||||
}
|
||||
_captureSource = OBSWeakSource{};
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetBufferDuration(double seconds)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_audioMutex);
|
||||
_bufferDurationSeconds = seconds;
|
||||
_audioBuffer.clear();
|
||||
_framesSinceLastStep = 0;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetNThreads(int n)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_nThreads = std::max(1, n);
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetLanguage(const std::string &lang)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_language = lang.empty() ? "auto" : lang;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetTranslate(bool translate)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_translate = translate;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetVadEnergyThreshold(float threshold)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_audioMutex);
|
||||
_vadEnergyThreshold = threshold;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetSuppressNonSpeechTokens(bool suppress)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_suppressNonSpeechTokens = suppress;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetNoContext(bool noContext)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_noContext = noContext;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetListenWhenMuted(bool listen)
|
||||
{
|
||||
_listenWhenMuted = listen;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetUseGpu(bool useGpu)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_ctxMutex);
|
||||
_useGpu = useGpu;
|
||||
}
|
||||
|
||||
std::shared_ptr<MessageBuffer<std::string>> SpeechRecognizer::RegisterClient()
|
||||
{
|
||||
return _dispatcher.RegisterClient();
|
||||
}
|
||||
|
||||
void SpeechRecognizer::AudioCaptureCallback(void *param, obs_source_t *,
|
||||
const struct audio_data *audio,
|
||||
bool muted)
|
||||
{
|
||||
if (!audio || !audio->data[0]) {
|
||||
return;
|
||||
}
|
||||
auto *self = static_cast<SpeechRecognizer *>(param);
|
||||
if (muted && !self->_listenWhenMuted) {
|
||||
return;
|
||||
}
|
||||
self->AppendResampledAudio(audio);
|
||||
}
|
||||
|
||||
void SpeechRecognizer::AppendResampledAudio(const struct audio_data *audio)
|
||||
{
|
||||
if (!_resampler) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *resampledData[MAX_AV_PLANES] = {};
|
||||
uint32_t outFrames = 0;
|
||||
uint64_t tsOffset = 0;
|
||||
|
||||
bool ok = audio_resampler_resample(
|
||||
static_cast<audio_resampler_t *>(_resampler), resampledData,
|
||||
&outFrames, &tsOffset, (const uint8_t *const *)audio->data,
|
||||
audio->frames);
|
||||
if (!ok || outFrames == 0 || !resampledData[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float *samples =
|
||||
reinterpret_cast<const float *>(resampledData[0]);
|
||||
|
||||
std::unique_lock<std::mutex> lock(_audioMutex);
|
||||
_audioBuffer.insert(_audioBuffer.end(), samples, samples + outFrames);
|
||||
_framesSinceLastStep += outFrames;
|
||||
|
||||
// Keep the rolling buffer capped at the configured context window.
|
||||
const size_t maxFrames =
|
||||
(size_t)(_bufferDurationSeconds * whisperSampleRate);
|
||||
if (_audioBuffer.size() > maxFrames) {
|
||||
_audioBuffer.erase(_audioBuffer.begin(),
|
||||
_audioBuffer.begin() +
|
||||
(_audioBuffer.size() - maxFrames));
|
||||
}
|
||||
|
||||
// Only consider triggering inference once per step interval.
|
||||
const size_t stepFrames =
|
||||
(size_t)(stepDurationSeconds * whisperSampleRate);
|
||||
if (_framesSinceLastStep < stepFrames) {
|
||||
return;
|
||||
}
|
||||
_framesSinceLastStep = 0;
|
||||
|
||||
// VAD: measure energy over just the most recent step window so that a
|
||||
// short utterance at the end of a longer silent buffer is not diluted.
|
||||
const size_t vadWindow = std::min(_audioBuffer.size(), stepFrames);
|
||||
const size_t vadStart = _audioBuffer.size() - vadWindow;
|
||||
float energy = 0.0f;
|
||||
for (size_t i = vadStart; i < _audioBuffer.size(); ++i) {
|
||||
energy += _audioBuffer[i] * _audioBuffer[i];
|
||||
}
|
||||
energy /= (float)vadWindow;
|
||||
const bool tooSilent = energy < _vadEnergyThreshold;
|
||||
if (tooSilent) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> infLock(_inferenceMutex);
|
||||
if (_bufferReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
_inferenceBuffer = _audioBuffer;
|
||||
_bufferReady = true;
|
||||
_inferenceCV.notify_one();
|
||||
}
|
||||
|
||||
// Retain a short overlap so the next inference has word-boundary context.
|
||||
const size_t keepFrames =
|
||||
(size_t)(keepDurationSeconds * whisperSampleRate);
|
||||
if (_audioBuffer.size() > keepFrames) {
|
||||
_audioBuffer.erase(_audioBuffer.begin(),
|
||||
_audioBuffer.begin() +
|
||||
(_audioBuffer.size() - keepFrames));
|
||||
}
|
||||
}
|
||||
|
||||
void SpeechRecognizer::InferenceLoop()
|
||||
{
|
||||
while (true) {
|
||||
std::vector<float> buffer;
|
||||
int nThreads;
|
||||
std::string language;
|
||||
bool translate;
|
||||
bool suppressNonSpeechTokens;
|
||||
bool noContext;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_inferenceMutex);
|
||||
_inferenceCV.wait(lock,
|
||||
[this] { return _bufferReady; });
|
||||
_bufferReady = false;
|
||||
if (_stopThread) {
|
||||
break;
|
||||
}
|
||||
buffer = std::move(_inferenceBuffer);
|
||||
nThreads = _nThreads;
|
||||
language = _language;
|
||||
translate = _translate;
|
||||
suppressNonSpeechTokens = _suppressNonSpeechTokens;
|
||||
noContext = _noContext;
|
||||
}
|
||||
|
||||
if (buffer.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> ctxLock(_ctxMutex);
|
||||
|
||||
if (!_ctx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
whisper_full_params params =
|
||||
whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
|
||||
params.print_realtime = false;
|
||||
params.print_progress = false;
|
||||
params.print_timestamps = false;
|
||||
params.print_special = false;
|
||||
params.translate = translate;
|
||||
params.language = language.c_str();
|
||||
params.n_threads = nThreads;
|
||||
params.single_segment = false;
|
||||
params.suppress_nst = suppressNonSpeechTokens;
|
||||
params.no_context = noContext;
|
||||
|
||||
// Limit the encoder to the actual audio length
|
||||
params.audio_ctx =
|
||||
std::min(1500, (int)((float)buffer.size() /
|
||||
(float)whisperSampleRate * 50.0f));
|
||||
|
||||
int rc = whisper_full(_ctx, params, buffer.data(),
|
||||
(int)buffer.size());
|
||||
if (rc != 0) {
|
||||
blog(LOG_WARNING, "whisper_full returned %d", rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string transcript;
|
||||
const int nSegments = whisper_full_n_segments(_ctx);
|
||||
for (int i = 0; i < nSegments; ++i) {
|
||||
const char *text =
|
||||
whisper_full_get_segment_text(_ctx, i);
|
||||
if (text) {
|
||||
transcript += text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!transcript.empty()) {
|
||||
const auto begin =
|
||||
transcript.find_first_not_of(" \t\r\n");
|
||||
if (begin != std::string::npos) {
|
||||
transcript = transcript.substr(begin);
|
||||
}
|
||||
_dispatcher.DispatchMessage(transcript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
86
plugins/speech/speech-recognizer.hpp
Normal file
86
plugins/speech/speech-recognizer.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "message-buffer.hpp"
|
||||
#include "message-dispatcher.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct whisper_context;
|
||||
struct audio_data;
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Captures audio from one OBS source, resamples to 16 kHz mono, runs
|
||||
// whisper.cpp inference on a background thread, and dispatches the resulting
|
||||
// transcript text to registered MessageBuffers.
|
||||
class SpeechRecognizer {
|
||||
public:
|
||||
SpeechRecognizer();
|
||||
~SpeechRecognizer();
|
||||
|
||||
bool LoadModel(const std::string &modelPath);
|
||||
|
||||
bool StartCapture(obs_source_t *source);
|
||||
void StopCapture();
|
||||
|
||||
void SetBufferDuration(double seconds);
|
||||
void SetNThreads(int n);
|
||||
void SetLanguage(const std::string &lang);
|
||||
void SetTranslate(bool translate);
|
||||
void SetVadEnergyThreshold(float threshold);
|
||||
void SetSuppressNonSpeechTokens(bool suppress);
|
||||
void SetNoContext(bool noContext);
|
||||
void SetListenWhenMuted(bool listen);
|
||||
void SetUseGpu(bool useGpu);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<MessageBuffer<std::string>>
|
||||
RegisterClient();
|
||||
|
||||
private:
|
||||
static void AudioCaptureCallback(void *param, obs_source_t *source,
|
||||
const struct audio_data *audio,
|
||||
bool muted);
|
||||
void AppendResampledAudio(const struct audio_data *audio);
|
||||
void InferenceLoop();
|
||||
|
||||
// Held during whisper_full and when freeing/replacing _ctx.
|
||||
std::mutex _ctxMutex;
|
||||
whisper_context *_ctx = nullptr;
|
||||
// Stored as void* to avoid pulling <media-io/audio-resampler.h> into
|
||||
// this header. Cast to audio_resampler_t* in the .cpp.
|
||||
void *_resampler = nullptr;
|
||||
|
||||
std::vector<float> _audioBuffer;
|
||||
std::mutex _audioMutex;
|
||||
double _bufferDurationSeconds = 5.0;
|
||||
float _vadEnergyThreshold = 1e-4f;
|
||||
size_t _framesSinceLastStep = 0;
|
||||
|
||||
std::vector<float> _inferenceBuffer;
|
||||
std::thread _inferenceThread;
|
||||
std::atomic_bool _stopThread{false};
|
||||
std::condition_variable _inferenceCV;
|
||||
std::mutex _inferenceMutex;
|
||||
bool _bufferReady = false;
|
||||
int _nThreads = 4;
|
||||
std::string _language = "auto";
|
||||
bool _translate = false;
|
||||
bool _suppressNonSpeechTokens = true;
|
||||
bool _noContext = true;
|
||||
bool _listenWhenMuted = false;
|
||||
bool _useGpu = true;
|
||||
|
||||
OBSWeakSource _captureSource;
|
||||
int _sourceSampleRate = 44100;
|
||||
int _sourceChannelCount = 2;
|
||||
|
||||
MessageDispatcher<std::string> _dispatcher;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user