From 8cd1846bba41b1482a66f89857203c713dd13ec3 Mon Sep 17 00:00:00 2001 From: pkv Date: Sat, 14 Jun 2025 15:55:54 +0200 Subject: [PATCH 1/4] cmake: Add finder for Steinberg VST3 SDK A finder to Steinberg VST3 SDK is added in cmake. Signed-off-by: pkv --- cmake/finders/FindVST3SDK.cmake | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 cmake/finders/FindVST3SDK.cmake diff --git a/cmake/finders/FindVST3SDK.cmake b/cmake/finders/FindVST3SDK.cmake new file mode 100644 index 00000000000000..f39cc26a949aca --- /dev/null +++ b/cmake/finders/FindVST3SDK.cmake @@ -0,0 +1,118 @@ +#[=======================================================================[.rst +FindVST3SDK +----------- + +FindModule for VST3 SDK + +Imported Targets +^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.0 + +This module defines the :prop_tgt:`IMPORTED` target ``VST3::SDK``. + + +Result Variables +^^^^^^^^^^^^^^^^ + +This module sets the following variables: + +``VST3SDK_FOUND`` + True, if all required components and the core library were found. + +``VST3SDK_PATH`` + Path to the SDK. + +``VST3SDK_REQUIRED_FILES`` + List of required files. +#]=======================================================================] + +include(FindPackageHandleStandardArgs) + +find_path( + VST3SDK_PATH + NAMES pluginterfaces/base/funknown.h + PATHS ${VST3SDK_PATH} ${CMAKE_SOURCE_DIR}/plugins/obs-vst3/sdk ${CMAKE_SOURCE_DIR}/deps/vst3sdk + NO_DEFAULT_PATH +) + +if(VST3SDK_PATH) + set( + VST3SDK_REQUIRED_FILES + pluginterfaces/base/funknown.cpp + pluginterfaces/base/coreiids.cpp + public.sdk/source/vst/vstinitiids.cpp + public.sdk/source/vst/hosting/connectionproxy.cpp + public.sdk/source/vst/hosting/eventlist.cpp + public.sdk/source/vst/hosting/hostclasses.cpp + public.sdk/source/vst/hosting/module.cpp + public.sdk/source/vst/hosting/parameterchanges.cpp + public.sdk/source/vst/hosting/pluginterfacesupport.cpp + public.sdk/source/vst/hosting/processdata.cpp + public.sdk/source/vst/hosting/plugprovider.cpp + public.sdk/source/vst/moduleinfo/moduleinfoparser.cpp + public.sdk/source/common/commonstringconvert.cpp + public.sdk/source/common/memorystream.cpp + public.sdk/source/vst/utility/stringconvert.cpp + ) + + if(OS_WINDOWS) + list( + APPEND VST3SDK_REQUIRED_FILES + public.sdk/source/vst/hosting/module_win32.cpp + public.sdk/source/common/threadchecker_win32.cpp + ) + elseif(OS_MACOS) + list( + APPEND VST3SDK_REQUIRED_FILES + public.sdk/source/vst/hosting/module_mac.mm + public.sdk/source/common/threadchecker_mac.mm + ) + elseif(OS_LINUX) + list( + APPEND VST3SDK_REQUIRED_FILES + public.sdk/source/vst/hosting/module_linux.cpp + public.sdk/source/common/threadchecker_linux.cpp + ) + endif() + + set(_vst3sdk_missing "") + foreach(_f IN LISTS VST3SDK_REQUIRED_FILES) + if(NOT EXISTS "${VST3SDK_PATH}/${_f}") + list(APPEND _vst3sdk_missing "${_f}") + endif() + endforeach() + + if(_vst3sdk_missing) + message(STATUS "VST3 SDK candidate at ${VST3SDK_PATH} is incomplete:") + foreach(_m IN LISTS _vst3sdk_missing) + message(STATUS " Missing: ${_m}") + endforeach() + set(VST3SDK_FOUND FALSE) + else() + set(VST3SDK_FOUND TRUE) + endif() +endif() + +find_package_handle_standard_args( + VST3SDK + REQUIRED_VARS VST3SDK_PATH VST3SDK_FOUND + REASON_FAILURE_MESSAGE + "Could not find a complete Steinberg VST3 SDK. Set VST3SDK_PATH to the SDK root containing base, pluginterfaces, public.sdk/source/vst/hosting." +) + +if(VST3SDK_FOUND) + if(NOT TARGET VST3::SDK) + add_library(VST3::SDK INTERFACE IMPORTED) + set_target_properties(VST3::SDK PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${VST3SDK_PATH}") + message(STATUS "Found VST3 SDK: ${VST3SDK_PATH}") + endif() +endif() + +include(FeatureSummary) +set_package_properties( + VST3SDK + PROPERTIES + URL "https://www.steinberg.net/developers/" + DESCRIPTION "The Steinberg VST3 SDK provides the headers and sources for hosting and developing VST3 plug-ins." +) From 030f3a82a96a275f7b83a77652a02b795c36be9f Mon Sep 17 00:00:00 2001 From: pkv Date: Sat, 1 Nov 2025 02:04:02 +0100 Subject: [PATCH 2/4] CI: Enable VST3 SDK This pulls the VST3 SDK for CI builds. Signed-off-by: pkv --- .github/scripts/.build.zsh | 6 ++++++ .github/scripts/Build-Windows.ps1 | 7 +++++-- .github/scripts/utils.zsh/setup_ubuntu | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/scripts/.build.zsh b/.github/scripts/.build.zsh index ab417cebb88fdd..89ad6f268d083b 100755 --- a/.github/scripts/.build.zsh +++ b/.github/scripts/.build.zsh @@ -40,6 +40,7 @@ build() { if (( ! ${+SCRIPT_HOME} )) typeset -g SCRIPT_HOME=${ZSH_ARGZERO:A:h} local host_os=${${(s:-:)ZSH_ARGZERO:t:r}[2]} local project_root=${SCRIPT_HOME:A:h:h} + local buildspec_file=${project_root}/CMakePresets.json fpath=(${SCRIPT_HOME}/utils.zsh ${fpath}) autoload -Uz log_group log_error log_output check_${host_os} @@ -126,6 +127,10 @@ build() { cmake_args+=(CMAKE_XCODE_ATTRIBUTE_COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS:STRING=YES) } + local deps_version + read -r deps_version <<< "$(jq -r '.obsproject.com/obs-studio.dependencies.prebuilt.version' "${buildspec_file}")" + cmake_args+=(-DVST3SDK_PATH:STRING=${project_root}/.deps/obs-deps-${deps_version}-universal/include/vst3sdk) + typeset -gx NSUnbufferedIO=YES typeset -gx CODESIGN_IDENT="${CODESIGN_IDENT:--}" @@ -213,6 +218,7 @@ build() { --preset ubuntu-ci -DENABLE_BROWSER:BOOL=ON -DCEF_ROOT_DIR:PATH="${project_root}/.deps/cef_binary_${CEF_VERSION}_${target//ubuntu-/linux_}" + -DVST3SDK_PATH:PATH="${project_root}/.deps/vst3sdk" ) cmake_build_args+=(build_${target%%-*} --config ${config} --parallel) diff --git a/.github/scripts/Build-Windows.ps1 b/.github/scripts/Build-Windows.ps1 index c8eb6bb74de2f6..c90992695a5db6 100644 --- a/.github/scripts/Build-Windows.ps1 +++ b/.github/scripts/Build-Windows.ps1 @@ -36,14 +36,14 @@ function Build { $ScriptHome = $PSScriptRoot $ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.." - + $BuildSpecFile = "${ProjectRoot}/CMakePresets.json" $UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse foreach($Utility in $UtilityFunctions) { Write-Debug "Loading $($Utility.FullName)" . $Utility.FullName } - + $BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile" Push-Location -Stack BuildTemp @@ -51,6 +51,9 @@ function Build { $CmakeArgs = @('--preset', "windows-ci-${Target}") + $DepsVersion = $BuildSpec.configurePresets.vendor.'obsproject.com/obs-studio'.dependencies.prebuilt.version + $CmakeArgs += @("-DVST3SDK_PATH=${ProjectRoot}\.deps\obs-deps-${DepsVersion}-${Target}\include\vst3sdk") + $CmakeBuildArgs = @('--build') $CmakeInstallArgs = @() diff --git a/.github/scripts/utils.zsh/setup_ubuntu b/.github/scripts/utils.zsh/setup_ubuntu index d877352c27a312..151f68d143a008 100644 --- a/.github/scripts/utils.zsh/setup_ubuntu +++ b/.github/scripts/utils.zsh/setup_ubuntu @@ -92,3 +92,27 @@ sudo apt-get install -y --no-install-recommends \ libffmpeg-nvenc-dev librist-dev libsrt-openssl-dev \ qt6-base-dev libqt6svg6-dev qt6-base-private-dev \ libvpl-dev libvpl2 + +log_group 'Setting up VST3 SDK...' + +pushd "${project_root}/.deps" + +local _target="vst3sdk" +local _url="https://github.com/steinbergmedia/vst3sdk.git" +local _hash="9fad9770f2ae8542ab1a548a68c1ad1ac690abe0" + +if [[ ! -d ${_target}/pluginterfaces ]]; then + log_status "Cloning Steinberg VST3 SDK..." + git clone --filter=blob:none --sparse --recurse-submodules=no "${_url}" "${_target}" + + pushd "${_target}" + git checkout "${_hash}" + git submodule update --init base pluginterfaces public.sdk + popd + + log_status "Steinberg VST3 SDK cloned successfully." +else + log_status "Steinberg VST3 SDK already present; skipping clone." +fi + +popd From 9219f8e578ad17836760c9e56b3dc31022e6826b Mon Sep 17 00:00:00 2001 From: pkv Date: Thu, 27 Nov 2025 11:59:17 +0100 Subject: [PATCH 3/4] build-aux: Disable obs-vst3 for flatpak This disables obs-vst3 in the flatpak manifest. Flatpak can't be supported for now because Steinberg SDK does not allow locations other than in /usr and $HOME for the VST3s. Signed-off-by: pkv --- build-aux/com.obsproject.Studio.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build-aux/com.obsproject.Studio.json b/build-aux/com.obsproject.Studio.json index 9ff7c8652989b0..a2dbf6c2e2c48e 100644 --- a/build-aux/com.obsproject.Studio.json +++ b/build-aux/com.obsproject.Studio.json @@ -141,7 +141,8 @@ "-DENABLE_AJA=ON", "-DENABLE_LIBFDK=ON", "-DENABLE_QSV11=ON", - "-DENABLE_DECKLINK=OFF" + "-DENABLE_DECKLINK=OFF", + "-DENABLE_VST3=OFF" ], "secret-opts": [ "-DRESTREAM_CLIENTID=$RESTREAM_CLIENTID", From 9a4a8310a2626632c7873286823593ff5600b4a2 Mon Sep 17 00:00:00 2001 From: pkv Date: Wed, 21 May 2025 18:34:08 +0200 Subject: [PATCH 4/4] obs-vst3: Add a VST3 host This adds a VST3 host to obs-studio on Windows, macOS and Linux. Only audio effects are enabled. Instruments and MIDI are not supported. A maximum of 1 main bus, 1 sidechain input bus and 1 main output bus are enabled. Note that VST3 on linux only support X11. Although the SDK added Wayland capability, it is too recent so there is yet no Wayland VST3 apart from a demo from the SDK. Signed-off-by: pkv --- plugins/CMakeLists.txt | 1 + plugins/obs-vst3/CMakeLists.txt | 145 ++ plugins/obs-vst3/RunLoopImpl.cpp | 246 ++++ plugins/obs-vst3/RunLoopImpl.h | 65 + plugins/obs-vst3/VST3ComponentHolder.cpp | 116 ++ plugins/obs-vst3/VST3ComponentHolder.h | 59 + plugins/obs-vst3/VST3EditorWindow.cpp | 517 +++++++ plugins/obs-vst3/VST3EditorWindow.h | 89 ++ plugins/obs-vst3/VST3HostApp.cpp | 96 ++ plugins/obs-vst3/VST3HostApp.h | 73 + plugins/obs-vst3/VST3Plugin.cpp | 623 +++++++++ plugins/obs-vst3/VST3Plugin.h | 133 ++ plugins/obs-vst3/VST3Scanner.cpp | 333 +++++ plugins/obs-vst3/VST3Scanner.h | 62 + .../obs-vst3/cmake/windows/obs-module.rc.in | 27 + plugins/obs-vst3/cmake/windows/obs-studio.ico | Bin 0 -> 93455 bytes plugins/obs-vst3/data/locale/en-US.ini | 7 + plugins/obs-vst3/obs-vst3.cpp | 1209 +++++++++++++++++ plugins/obs-vst3/obs-vst3.h | 83 ++ plugins/obs-vst3/plugin-main.cpp | 64 + 20 files changed, 3948 insertions(+) create mode 100644 plugins/obs-vst3/CMakeLists.txt create mode 100644 plugins/obs-vst3/RunLoopImpl.cpp create mode 100644 plugins/obs-vst3/RunLoopImpl.h create mode 100644 plugins/obs-vst3/VST3ComponentHolder.cpp create mode 100644 plugins/obs-vst3/VST3ComponentHolder.h create mode 100644 plugins/obs-vst3/VST3EditorWindow.cpp create mode 100644 plugins/obs-vst3/VST3EditorWindow.h create mode 100644 plugins/obs-vst3/VST3HostApp.cpp create mode 100644 plugins/obs-vst3/VST3HostApp.h create mode 100644 plugins/obs-vst3/VST3Plugin.cpp create mode 100644 plugins/obs-vst3/VST3Plugin.h create mode 100644 plugins/obs-vst3/VST3Scanner.cpp create mode 100644 plugins/obs-vst3/VST3Scanner.h create mode 100644 plugins/obs-vst3/cmake/windows/obs-module.rc.in create mode 100644 plugins/obs-vst3/cmake/windows/obs-studio.ico create mode 100644 plugins/obs-vst3/data/locale/en-US.ini create mode 100644 plugins/obs-vst3/obs-vst3.cpp create mode 100644 plugins/obs-vst3/obs-vst3.h create mode 100644 plugins/obs-vst3/plugin-main.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1bea2e4ff92d02..9357b7d9908905 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -91,6 +91,7 @@ add_obs_plugin( PLATFORMS WINDOWS MACOS LINUX WITH_MESSAGE ) +add_obs_plugin(obs-vst3 PLATFORMS WINDOWS MACOS LINUX) add_obs_plugin(obs-webrtc) check_obs_websocket() diff --git a/plugins/obs-vst3/CMakeLists.txt b/plugins/obs-vst3/CMakeLists.txt new file mode 100644 index 00000000000000..752cd7cb3bdd1f --- /dev/null +++ b/plugins/obs-vst3/CMakeLists.txt @@ -0,0 +1,145 @@ +cmake_minimum_required(VERSION 3.28...3.30) + +option(ENABLE_VST3 "Enable building OBS with VST3 plugin" ON) + +if(NOT ENABLE_VST3) + target_disable(obs-vst3) + return() +endif() + +project(obs-vst3) + +find_package(Qt6 REQUIRED Widgets) +set(CMAKE_AUTOMOC ON) + +add_library(obs-vst3 MODULE) +add_library(OBS::vst3 ALIAS obs-vst3) + +find_package(VST3SDK QUIET) + +if(NOT VST3SDK_FOUND) + message(STATUS "VST3 SDK not found — disabling obs-vst3 plugin.") + target_disable(obs-vst3) + return() +endif() + +# SDK compile warnings +set( + SDK_WARN_FLAGS + -Wno-cast-align + -Wno-conversion + -Wno-cpp + -Wno-delete-non-virtual-dtor + -Wno-deprecated + -Wno-deprecated-copy-dtor + -Wno-deprecated-declarations + -Wno-dangling-else + -Wno-extra + -Wno-extra-semi + -Wno-float-equal + -Wno-format + -Wno-format-security + -Wno-format-truncation + -Wno-ignored-qualifiers + -Wno-int-to-pointer-cast + -Wno-missing-braces + -Wno-missing-field-initializers + -Wno-non-virtual-dtor + -Wno-overloaded-virtual + -Wno-parentheses + -Wno-pedantic + -Wno-redundant-decls + -Wno-reorder + -Wno-shadow + -Wno-sign-compare + -Wno-sign-conversion + -Wno-switch-default + -Wno-type-limits + -Wno-unused-but-set-variable + -Wno-unused-function + -Wno-unused-parameter + -Wno-zero-as-null-pointer-constant +) + +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + list(APPEND SDK_WARN_FLAGS -Wno-class-memaccess -Wno-maybe-uninitialized) +endif() + +list(JOIN SDK_WARN_FLAGS " " SDK_WARN_FLAGS_STR) + +# SDK sources +set(VST3_SDK_SOURCES) +foreach(_src IN LISTS VST3SDK_REQUIRED_FILES) + list(APPEND VST3_SDK_SOURCES "${VST3SDK_PATH}/${_src}") +endforeach() + +# Main sources +set( + VST3_MAIN_SOURCES + plugin-main.cpp + obs-vst3.cpp + obs-vst3.h + VST3ComponentHolder.cpp + VST3ComponentHolder.h + VST3HostApp.cpp + VST3HostApp.h + VST3Scanner.cpp + VST3Scanner.h + VST3Plugin.cpp + VST3Plugin.h + VST3EditorWindow.cpp + VST3EditorWindow.h +) + +# Editor window sources +set(VST3EDITORWINDOW_SRC) +if(OS_LINUX) + list(APPEND VST3EDITORWINDOW_SRC RunLoopImpl.cpp) +endif() + +# Add all sources +target_sources(obs-vst3 PRIVATE ${VST3_MAIN_SOURCES} ${VST3EDITORWINDOW_SRC} ${VST3_SDK_SOURCES}) + +# Disable warnings for SDK files +if(OS_LINUX OR OS_MACOS) + set_source_files_properties(${VST3_SDK_SOURCES} PROPERTIES COMPILE_FLAGS "${SDK_WARN_FLAGS_STR}") +endif() + +# macOS ARC for all .mm files (editor + sdk) +if(OS_MACOS) + set_source_files_properties(VST3EditorWindow.cpp PROPERTIES LANGUAGE OBJCXX COMPILE_OPTIONS "-fobjc-arc") + set_source_files_properties( + "${VST3SDK_PATH}/public.sdk/source/vst/hosting/module_mac.mm" + "${VST3SDK_PATH}/public.sdk/source/common/threadchecker_mac.mm" + PROPERTIES COMPILE_OPTIONS "-fobjc-arc" COMPILE_FLAGS "${SDK_WARN_FLAGS_STR}" + ) + target_link_libraries(obs-vst3 PRIVATE "-framework Cocoa" "-framework Foundation") +endif() + +# Includes +target_include_directories( + obs-vst3 + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${VST3SDK_PATH} + ${VST3SDK_PATH}/base + ${VST3SDK_PATH}/pluginterfaces + ${VST3SDK_PATH}/public.sdk/source/vst + ${VST3SDK_PATH}/public.sdk/source/vst/hosting + ${VST3SDK_PATH}/public.sdk/source/vst/utility +) + +target_link_libraries(obs-vst3 PRIVATE OBS::libobs Qt6::Widgets) + +# Windows resources +if(OS_WINDOWS) + configure_file(cmake/windows/obs-module.rc.in obs-vst3.rc) + target_sources(obs-vst3 PRIVATE obs-vst3.rc) + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows/obs-studio.ico + ${CMAKE_CURRENT_BINARY_DIR}/obs-studio.ico + COPYONLY + ) +endif() + +set_target_properties_obs(obs-vst3 PROPERTIES FOLDER plugins PREFIX "") diff --git a/plugins/obs-vst3/RunLoopImpl.cpp b/plugins/obs-vst3/RunLoopImpl.cpp new file mode 100644 index 00000000000000..8b46cba1481dee --- /dev/null +++ b/plugins/obs-vst3/RunLoopImpl.cpp @@ -0,0 +1,246 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "RunLoopImpl.h" + +#include + +#include + +DEF_CLASS_IID(Steinberg::Linux::IRunLoop) + +RunLoopImpl::RunLoopImpl() = default; + +void RunLoopImpl::updateTimer(TimerSlot *slot) +{ + if (stopping || !slot) { + return; + } + + auto *handler = slot->handler; + if (handler) { + handler->addRef(); + handler->onTimer(); + handler->release(); + } +} + +void RunLoopImpl::dispatchFD(int fd) +{ + if (stopping) { + return; + } + + Steinberg::Linux::IEventHandler *handler = nullptr; + { + std::lock_guard lock(eventMutex); + auto it = fdHandlers.find(fd); + if (it == fdHandlers.end() || it->second == nullptr) { + return; + } + handler = it->second; + handler->addRef(); + } + handler->onFDIsSet(fd); + handler->release(); +} + +Steinberg::tresult RunLoopImpl::registerEventHandler(Steinberg::Linux::IEventHandler *handler, int fd) +{ + if (!handler || fd < 0) { + return Steinberg::kInvalidArgument; + } + { + std::lock_guard lock(eventMutex); + if (fdHandlers.count(fd)) { + return Steinberg::kInvalidArgument; + } + fdHandlers[fd] = handler; + handler->addRef(); + } + auto *notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this); + + QObject::connect(notifier, &QSocketNotifier::activated, this, [this, fd] { dispatchFD(fd); }); + + { + std::lock_guard lock(eventMutex); + fdReadNotifiers[fd] = notifier; + } + + return Steinberg::kResultTrue; +} + +Steinberg::tresult PLUGIN_API RunLoopImpl::unregisterEventHandler(Steinberg::Linux::IEventHandler *handler) +{ + if (!handler) { + return Steinberg::kInvalidArgument; + } + + std::vector toRemoveFds; + std::vector toDeleteNotifiers; + + { + std::lock_guard lock(eventMutex); + for (auto it = fdHandlers.begin(); it != fdHandlers.end();) { + if (it->second == handler) { + int fd = it->first; + toRemoveFds.push_back(fd); + + auto notifierIt = fdReadNotifiers.find(fd); + if (notifierIt != fdReadNotifiers.end()) { + toDeleteNotifiers.push_back(notifierIt->second); + fdReadNotifiers.erase(notifierIt); + } + + it = fdHandlers.erase(it); + } else { + ++it; + } + } + } + + if (toRemoveFds.empty()) { + return Steinberg::kResultFalse; + } + + for (QSocketNotifier *sn : toDeleteNotifiers) { + if (!sn) { + continue; + } + + sn->setEnabled(false); + sn->deleteLater(); + } + + for (size_t i = 0; i < toRemoveFds.size(); ++i) { + handler->release(); + } + + return Steinberg::kResultTrue; +} + +Steinberg::tresult RunLoopImpl::registerTimer(Steinberg::Linux::ITimerHandler *handler, uint64_t ms) +{ + if (!handler || ms == 0) { + return Steinberg::kInvalidArgument; + } + + for (const auto *slot : pluginTimers) { + if (slot->handler == handler) { + return Steinberg::kResultFalse; + } + } + handler->addRef(); + auto *slot = new TimerSlot{handler, new QTimer(this)}; + slot->qt->setInterval(static_cast(ms)); + QObject::connect(slot->qt, &QTimer::timeout, this, [this, slot]() { updateTimer(slot); }); + slot->qt->start(); + pluginTimers.push_back(slot); + + return Steinberg::kResultTrue; +} + +Steinberg::tresult RunLoopImpl::unregisterTimer(Steinberg::Linux::ITimerHandler *handler) +{ + if (!handler) { + return Steinberg::kInvalidArgument; + } + + auto it = std::find_if(pluginTimers.begin(), pluginTimers.end(), + [&](TimerSlot *slot) { return slot && slot->handler == handler; }); + + if (it == pluginTimers.end()) { + return Steinberg::kResultFalse; + } + + TimerSlot *slot = *it; + if (slot->qt) { + slot->qt->stop(); + slot->qt->disconnect(this); + slot->qt->deleteLater(); + } + if (slot->handler) { + slot->handler->release(); + } + delete slot; + pluginTimers.erase(it); + return Steinberg::kResultTrue; +} + +uint32_t RunLoopImpl::addRef() +{ + return 1000; +} + +uint32_t RunLoopImpl::release() +{ + return 1000; +} + +Steinberg::tresult RunLoopImpl::queryInterface(const Steinberg::TUID iid, void **obj) +{ + if (Steinberg::FUnknownPrivate::iidEqual(iid, Steinberg::Linux::IRunLoop::iid) || + Steinberg::FUnknownPrivate::iidEqual(iid, Steinberg::FUnknown::iid)) { + *obj = static_cast(this); + return Steinberg::kResultOk; + } + *obj = nullptr; + return Steinberg::kNoInterface; +} + +RunLoopImpl::~RunLoopImpl() +{ + stopping = true; + + std::vector eventHandlers; + { + std::lock_guard lock(eventMutex); + + for (const auto &entry : fdReadNotifiers) { + auto *notifier = entry.second; + if (notifier) { + notifier->setEnabled(false); + } + } + fdReadNotifiers.clear(); + + for (const auto &entry : fdHandlers) { + auto *handler = entry.second; + if (handler) { + eventHandlers.push_back(handler); + } + } + fdHandlers.clear(); + } + + for (auto *handler : eventHandlers) { + handler->release(); + } + + for (auto *slot : pluginTimers) { + if (slot->qt) { + slot->qt->stop(); + delete slot->qt; + } + if (slot->handler) { + slot->handler->release(); + } + delete slot; + } + + pluginTimers.clear(); +} diff --git a/plugins/obs-vst3/RunLoopImpl.h b/plugins/obs-vst3/RunLoopImpl.h new file mode 100644 index 00000000000000..76604203e55930 --- /dev/null +++ b/plugins/obs-vst3/RunLoopImpl.h @@ -0,0 +1,65 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once +#include "pluginterfaces/gui/iplugview.h" + +#include +#include + +#include +#include +#include +#include + +class QSocketNotifier; + +class RunLoopImpl : public QObject, public Steinberg::Linux::IRunLoop { +public: + RunLoopImpl(); + ~RunLoopImpl() override; + + RunLoopImpl(const RunLoopImpl &) = delete; + RunLoopImpl &operator=(const RunLoopImpl &) = delete; + RunLoopImpl(RunLoopImpl &&) = delete; + RunLoopImpl &operator=(RunLoopImpl &&) = delete; + + Steinberg::tresult PLUGIN_API registerEventHandler(Steinberg::Linux::IEventHandler *handler, int fd) override; + Steinberg::tresult PLUGIN_API unregisterEventHandler(Steinberg::Linux::IEventHandler *handler) override; + Steinberg::tresult PLUGIN_API registerTimer(Steinberg::Linux::ITimerHandler *handler, uint64_t ms) override; + Steinberg::tresult PLUGIN_API unregisterTimer(Steinberg::Linux::ITimerHandler *handler) override; + Steinberg::tresult PLUGIN_API queryInterface(const Steinberg::TUID iid, void **obj) override; + uint32_t PLUGIN_API addRef() override; + uint32_t PLUGIN_API release() override; + + void stop() { stopping = true; } + +private: + struct TimerSlot { + Steinberg::Linux::ITimerHandler *handler; + QTimer *qt; + }; + + void dispatchFD(int fd); + void updateTimer(TimerSlot *slot); + + bool stopping = false; + std::mutex eventMutex; + std::map fdHandlers; + std::map fdReadNotifiers; + std::vector pluginTimers; +}; \ No newline at end of file diff --git a/plugins/obs-vst3/VST3ComponentHolder.cpp b/plugins/obs-vst3/VST3ComponentHolder.cpp new file mode 100644 index 00000000000000..773443f9776e0f --- /dev/null +++ b/plugins/obs-vst3/VST3ComponentHolder.cpp @@ -0,0 +1,116 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "VST3ComponentHolder.h" + +#include "VST3Plugin.h" +#include "obs-vst3.h" + +using namespace Steinberg; +using namespace Vst; + +VST3ComponentHolder::VST3ComponentHolder(VST3Plugin *plugin_) : plugin(plugin_) {} + +VST3ComponentHolder::~VST3ComponentHolder() noexcept {FUNKNOWN_DTOR} + +tresult PLUGIN_API VST3ComponentHolder::beginEdit(ParamID) +{ + return kResultOk; +} + +tresult PLUGIN_API VST3ComponentHolder::performEdit(ParamID id, ParamValue valueNormalized) +{ + if (guiToDsp) { + guiToDsp->addChange(id, valueNormalized, 0); + } + + return kResultOk; +} + +tresult PLUGIN_API VST3ComponentHolder::endEdit(ParamID) +{ + return kResultOk; +} + +tresult PLUGIN_API VST3ComponentHolder::restartComponent(int32 flags) +{ + if (!plugin) { + return kInvalidArgument; + } + + if (flags & kLatencyChanged) { + plugin->obsVst3Data->bypass.store(true, std::memory_order_relaxed); + + if (plugin->audioEffect) { + plugin->audioEffect->setProcessing(false); + } + + if (plugin->vstPlug) { + plugin->vstPlug->setActive(false); + plugin->vstPlug->setActive(true); + } + + if (plugin->audioEffect) { + plugin->audioEffect->setProcessing(true); + } + + uint32 latency = plugin->audioEffect ? plugin->audioEffect->getLatencySamples() : 0; + infovst3plugin("Latency of the plugin is %u samples", latency); + + plugin->obsVst3Data->bypass.store(false, std::memory_order_relaxed); + return kResultOk; + } + if (flags & kParamValuesChanged) { + if (plugin->editController) { + int32 count = plugin->editController->getParameterCount(); + for (int32 i = 0; i < count; ++i) { + Steinberg::Vst::ParameterInfo info{}; + if (plugin->editController->getParameterInfo(i, info) == kResultOk) { + ParamValue value = plugin->editController->getParamNormalized(info.id); + plugin->guiToDsp.addChange(info.id, value, 0); + } + } + } + return kResultOk; + } + + return kNotImplemented; +} + +tresult PLUGIN_API VST3ComponentHolder::notifyUnitSelection(UnitID) +{ + return kResultTrue; +} + +tresult PLUGIN_API VST3ComponentHolder::notifyProgramListChange(ProgramListID, int32) +{ + return kResultTrue; +} + +tresult PLUGIN_API VST3ComponentHolder::queryInterface(const TUID _iid, void **obj) +{ + if (FUnknownPrivate::iidEqual(_iid, IComponentHandler::iid)) { + *obj = static_cast(this); + return kResultOk; + } + if (FUnknownPrivate::iidEqual(_iid, IUnitHandler::iid)) { + *obj = static_cast(this); + return kResultOk; + } + *obj = nullptr; + return kNoInterface; +} diff --git a/plugins/obs-vst3/VST3ComponentHolder.h b/plugins/obs-vst3/VST3ComponentHolder.h new file mode 100644 index 00000000000000..5033a364dbf819 --- /dev/null +++ b/plugins/obs-vst3/VST3ComponentHolder.h @@ -0,0 +1,59 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "pluginterfaces/vst/ivstaudioprocessor.h" +#include "pluginterfaces/vst/ivstcomponent.h" +#include "pluginterfaces/vst/ivsteditcontroller.h" +#include "pluginterfaces/vst/ivstunits.h" +#include "public.sdk/source/vst/hosting/parameterchanges.h" + +using namespace Steinberg; +using namespace Vst; + +class VST3Plugin; + +class VST3ComponentHolder : public IComponentHandler, public IUnitHandler { +public: + explicit VST3ComponentHolder(VST3Plugin *plugin_); + ~VST3ComponentHolder() noexcept; + + VST3ComponentHolder(const VST3ComponentHolder &) = delete; + VST3ComponentHolder &operator=(const VST3ComponentHolder &) = delete; + VST3ComponentHolder(VST3ComponentHolder &&) = delete; + VST3ComponentHolder &operator=(VST3ComponentHolder &&) = delete; + + ParameterChangeTransfer *guiToDsp = nullptr; + + tresult PLUGIN_API beginEdit(ParamID) override; + tresult PLUGIN_API performEdit(ParamID id, ParamValue valueNormalized) override; + tresult PLUGIN_API endEdit(ParamID) override; + tresult PLUGIN_API restartComponent(int32 flags) override; + + tresult PLUGIN_API notifyUnitSelection(UnitID) override; + tresult PLUGIN_API notifyProgramListChange(ProgramListID, int32) override; + + tresult PLUGIN_API queryInterface(const TUID _iid, void **obj) override; + // we do not care here of the ref-counting. A plug-in call of release should not destroy this class ! + uint32 PLUGIN_API addRef() override { return 1000; } + uint32 PLUGIN_API release() override { return 1000; } + + IComponentHandler *getComponentHandler() noexcept { return static_cast(this); } + + VST3Plugin *plugin = nullptr; +}; diff --git a/plugins/obs-vst3/VST3EditorWindow.cpp b/plugins/obs-vst3/VST3EditorWindow.cpp new file mode 100644 index 00000000000000..74bd5c43de4dc4 --- /dev/null +++ b/plugins/obs-vst3/VST3EditorWindow.cpp @@ -0,0 +1,517 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "VST3EditorWindow.h" +#ifdef __linux__ +#include "VST3HostApp.h" +#endif +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#endif + +#ifdef __linux__ +#include +#endif + +#include +#include +#include + +#ifdef _WIN32 +#include + +constexpr int obsIconId = 101; +#endif + +#ifdef __APPLE__ +#import +#endif + +#ifdef __linux__ +VST3HostApp *get_host_app() noexcept; +#endif + +using namespace Steinberg; + +namespace { + +bool sameSize(const ViewRect &lhs, const ViewRect &rhs) +{ + return lhs.getWidth() == rhs.getWidth() && lhs.getHeight() == rhs.getHeight(); +} + +void reportError(const char *message) +{ + std::fprintf(stderr, "VST3 editor error: %s\n", message); +} + +} // namespace + +#ifdef _WIN32 +HMODULE getCurrentModule() +{ + HMODULE module = nullptr; + + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&getCurrentModule), &module); + + return module; +} +#endif + +class VST3EditorWindow::PlugFrameImpl : public IPlugFrame { +public: + explicit PlugFrameImpl(VST3EditorWindow *window) : window_(window) {} + tresult PLUGIN_API resizeView(IPlugView *view, ViewRect *newSize) override + { + if (!window_ || !view || view != window_->view_ || !newSize || newSize->getWidth() <= 0 || + newSize->getHeight() <= 0) { + return kInvalidArgument; + } + + return window_->resizeFromPlugin(*newSize); + } + tresult PLUGIN_API queryInterface(const TUID _iid, void **obj) override + { + if (FUnknownPrivate::iidEqual(_iid, IPlugFrame::iid)) { + *obj = static_cast(this); + return kResultOk; + } +#ifdef __linux__ + if (get_host_app() && FUnknownPrivate::iidEqual(_iid, Linux::IRunLoop::iid)) { + return get_host_app()->queryInterface(_iid, obj); + } +#endif + *obj = nullptr; + return kNoInterface; + } + // refcounting does not matter here, cf SDK + uint32_t PLUGIN_API addRef() override { return 1; } + uint32_t PLUGIN_API release() override { return 1; } + +private: + VST3EditorWindow *window_; +}; + +VST3EditorWindow::VST3EditorWindow(IPlugView *view, const std::string &title) : QWidget(nullptr), view_(view) +{ + viewContainer_ = new QWidget(this); + + resizable_ = view_->canResize() == kResultTrue; + + Qt::WindowFlags flags = Qt::Tool | Qt::WindowTitleHint | Qt::WindowCloseButtonHint | + Qt::WindowMinimizeButtonHint; + + if (resizable_) { + flags |= Qt::WindowMaximizeButtonHint; + } + + setWindowFlags(flags); + setWindowTitle(QString::fromUtf8(title.data(), static_cast(title.size()))); + +#ifdef __APPLE__ + setAttribute(Qt::WA_MacAlwaysShowToolWindow); +#endif +#ifdef _WIN32 + if (const HICON icon = LoadIconW(getCurrentModule(), MAKEINTRESOURCEW(obsIconId))) { + setWindowIcon(QIcon(QPixmap::fromImage(QImage::fromHICON(icon)))); + } +#endif +#ifdef __linux__ + resizeLinuxTimer_ = new QTimer(this); + resizeLinuxTimer_->setSingleShot(true); + resizeLinuxTimer_->setInterval(10); + connect(resizeLinuxTimer_, &QTimer::timeout, this, [this] { handleLinuxResize(); }); +#endif +} + +VST3EditorWindow::~VST3EditorWindow() = default; + +bool VST3EditorWindow::create(int width, int height) +{ + if (!view_ || attached_ || width <= 0 || height <= 0) { + return false; + } + + ViewRect initialSize(0, 0, width, height); + view_->checkSizeConstraint(&initialSize); + const QSize qtInitialSize = vst3ToQtSize(initialSize); + if (resizable_) { + resize(qtInitialSize); + } else { + setFixedSize(qtInitialSize); + } + + viewContainer_->setGeometry(0, 0, qtInitialSize.width(), qtInitialSize.height()); + + QWidget::create(); +#ifdef __APPLE__ + if (resizable_) { + setMacContentAspectRatio(qtInitialSize); + } +#endif + const WId nativeParentId = viewContainer_->winId(); + if (!windowHandle() || !nativeParentId) { + reportError("failed to create the native window"); + return false; + } + + connect(windowHandle(), &QWindow::screenChanged, this, [this] { handleScaleChange(); }); + + frame_ = std::make_unique(this); + view_->setFrame(frame_.get()); + void *nativeParent = reinterpret_cast(nativeParentId); +#ifdef _WIN32 + const FIDString platformType = kPlatformTypeHWND; +#elif defined(__APPLE__) + const FIDString platformType = kPlatformTypeNSView; +#else + const FIDString platformType = kPlatformTypeX11EmbedWindowID; +#endif + + if (view_->attached(nativeParent, platformType) != kResultOk) { + view_->setFrame(nullptr); + frame_.reset(); + reportError("failed to attach the plug-in view"); + + return false; + } + + attached_ = true; + + ViewRect windowSize = qtToVst3Rect(viewContainer_->size()); + view_->onSize(&windowSize); + + (void)updateContentScaleFactor(); + + return true; +} + +void VST3EditorWindow::show() +{ + if (!attached_) { + return; + } + + QWidget::show(); + raise(); + activateWindow(); + + wasClosed_ = false; + + handleScaleChange(); +} + +void VST3EditorWindow::close() +{ + hide(); +} + +bool VST3EditorWindow::getClosedState() const +{ + return wasClosed_; +} + +bool VST3EditorWindow::event(QEvent *event) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) && (defined(_WIN32) || defined(__APPLE__)) + if (event->type() == QEvent::DevicePixelRatioChange) { + handlingDpiChange_ = true; + + const bool handled = QWidget::event(event); + const bool scaleChanged = updateContentScaleFactor(); + + handlingDpiChange_ = false; + + if (scaleChanged && attached_) { + ViewRect actualRect = qtToVst3Rect(viewContainer_->size()); + view_->onSize(&actualRect); + } + + return handled; + } +#endif + return QWidget::event(event); +} + +void VST3EditorWindow::closeEvent(QCloseEvent *event) +{ + wasClosed_ = true; + hide(); + event->ignore(); +} + +void VST3EditorWindow::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + viewContainer_->setGeometry(0, 0, event->size().width(), event->size().height()); + + if (!view_ || !attached_ || resizingFromPlugin_ || correctingHostResize_ || handlingDpiChange_) { + return; + } + +#ifdef _WIN32 + ViewRect actualRect = qtToVst3Rect(viewContainer_->size()); + view_->onSize(&actualRect); +#elif defined(__APPLE__) + ViewRect requested = qtToVst3Rect(event->size()); + ViewRect constrained = requested; + + if (resizable_) { + view_->checkSizeConstraint(&constrained); + } + + if (!sameSize(requested, constrained)) { + correctingHostResize_ = true; + resize(vst3ToQtSize(constrained)); + correctingHostResize_ = false; + } + + ViewRect actualRect = qtToVst3Rect(viewContainer_->size()); + view_->onSize(&actualRect); +#else + resizeLinuxTimer_->start(); +#endif +} + +#ifdef _WIN32 +bool VST3EditorWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) +{ + if (eventType != QByteArrayLiteral("windows_generic_MSG")) { + return QWidget::nativeEvent(eventType, message, result); + } + + auto *msg = static_cast(message); + + if (!view_ || !attached_ || !resizable_ || msg->hwnd != reinterpret_cast(winId()) || + msg->message != WM_SIZING) { + return QWidget::nativeEvent(eventType, message, result); + } + + auto *windowRect = reinterpret_cast(msg->lParam); + if (!windowRect) { + return QWidget::nativeEvent(eventType, message, result); + } + + const LONG style = GetWindowLong(msg->hwnd, GWL_STYLE); + const LONG exStyle = GetWindowLong(msg->hwnd, GWL_EXSTYLE); + + RECT nonClientRect{0, 0, 0, 0}; + AdjustWindowRectEx(&nonClientRect, style, FALSE, exStyle); + + const int extraWidth = nonClientRect.right - nonClientRect.left; + const int extraHeight = nonClientRect.bottom - nonClientRect.top; + + const int windowWidth = windowRect->right - windowRect->left; + const int windowHeight = windowRect->bottom - windowRect->top; + + ViewRect constrained(0, 0, windowWidth - extraWidth, windowHeight - extraHeight); + view_->checkSizeConstraint(&constrained); + + const int newWindowWidth = constrained.getWidth() + extraWidth; + const int newWindowHeight = constrained.getHeight() + extraHeight; + + switch (msg->wParam) { + case WMSZ_LEFT: + case WMSZ_TOPLEFT: + case WMSZ_BOTTOMLEFT: + windowRect->left = windowRect->right - newWindowWidth; + break; + + default: + windowRect->right = windowRect->left + newWindowWidth; + break; + } + + switch (msg->wParam) { + case WMSZ_TOP: + case WMSZ_TOPLEFT: + case WMSZ_TOPRIGHT: + windowRect->top = windowRect->bottom - newWindowHeight; + break; + + default: + windowRect->bottom = windowRect->top + newWindowHeight; + break; + } + + if (result) { + *result = TRUE; + } + + return true; +} +#endif + +#ifdef __APPLE__ +void VST3EditorWindow::setMacContentAspectRatio(const QSize &size) +{ + if (size.isEmpty()) { + return; + } + + NSView *nativeView = (__bridge NSView *)(reinterpret_cast(winId())); + + if (!nativeView || !nativeView.window) { + return; + } + + nativeView.window.contentAspectRatio = + NSMakeSize(static_cast(size.width()), static_cast(size.height())); +} +#endif + +qreal VST3EditorWindow::vst3CoordinateScaleFactor() const +{ +#if defined(_WIN32) + return windowHandle() ? windowHandle()->devicePixelRatio() : devicePixelRatioF(); +#elif defined(__linux__) + if (QGuiApplication::platformName() == QStringLiteral("xcb")) { + return windowHandle() ? windowHandle()->devicePixelRatio() : devicePixelRatioF(); + } +#endif + return 1.0; +} + +QSize VST3EditorWindow::vst3ToQtSize(const ViewRect &rect) const +{ + const qreal scale = vst3CoordinateScaleFactor(); + const int width = std::max(1, static_cast(std::lround(rect.getWidth() / scale))); + const int height = std::max(1, static_cast(std::lround(rect.getHeight() / scale))); + + return {width, height}; +} + +ViewRect VST3EditorWindow::qtToVst3Rect(const QSize &size) const +{ + const qreal scale = vst3CoordinateScaleFactor(); + const auto width = static_cast(std::lround(size.width() * scale)); + const auto height = static_cast(std::lround(size.height() * scale)); + + return {0, 0, std::max(1, width), std::max(1, height)}; +} + +tresult PLUGIN_API VST3EditorWindow::resizeFromPlugin(const ViewRect &rect) +{ + if (!view_ || resizeViewRecursionGuard_ || rect.getWidth() <= 0 || rect.getHeight() <= 0) { + return kResultFalse; + } + + const QSize targetSize = vst3ToQtSize(rect); + + if (viewContainer_->size() == targetSize) { + return kResultTrue; + } + + resizeViewRecursionGuard_ = true; + resizingFromPlugin_ = true; + + if (resizable_) { + resize(targetSize); + } else { + setFixedSize(targetSize); + } + + resizingFromPlugin_ = false; + + ViewRect actualRect = qtToVst3Rect(viewContainer_->size()); + view_->onSize(&actualRect); + + resizeViewRecursionGuard_ = false; + + return kResultTrue; +} + +#ifdef __linux__ +// TODO: improve X11 handling. It gave me a lot of headaches. Currently we use a throttle timer to call the plugin +// onSize. Without it I had repaint issues. Known BUG: currently there's still repaint issues with LSP VST3s when +// they are resized with their inner handle but not when the container is resized. Other VST3s on linux are fine. +void VST3EditorWindow::handleLinuxResize() +{ + if (!view_ || !attached_ || resizingFromPlugin_ || correctingHostResize_) { + return; + } + + const QSize requestedQtSize = viewContainer_->size(); + ViewRect requestedRect = qtToVst3Rect(requestedQtSize); + ViewRect constrainedRect = requestedRect; + + tresult constraintResult = kResultFalse; + + if (resizable_) { + constraintResult = view_->checkSizeConstraint(&constrainedRect); + } + + if (constraintResult == kResultTrue && !sameSize(requestedRect, constrainedRect)) { + const QSize constrainedQtSize = vst3ToQtSize(constrainedRect); + + correctingHostResize_ = true; + if (constrainedRect.getWidth() > requestedRect.getWidth() && + constrainedRect.getHeight() > requestedRect.getHeight()) { + setMinimumSize(constrainedQtSize); + } else { + resize(constrainedQtSize); + } + correctingHostResize_ = false; + + ViewRect actualRect = qtToVst3Rect(viewContainer_->size()); + view_->onSize(&actualRect); + return; + } + view_->onSize(&requestedRect); +} +#endif + +bool VST3EditorWindow::updateContentScaleFactor() +{ +#if defined(_WIN32) || defined(__APPLE__) + const qreal scale = devicePixelRatioF(); + + if (contentScaleFactor_ == scale) { + return false; + } + + contentScaleFactor_ = scale; + + FUnknownPtr scaleSupport(view_); + + if (scaleSupport) { + scaleSupport->setContentScaleFactor(static_cast(scale)); + } + + return true; +#else + return false; +#endif +} + +void VST3EditorWindow::handleScaleChange() +{ + if (updateContentScaleFactor() && attached_) { + ViewRect currentSize = qtToVst3Rect(viewContainer_->size()); + view_->onSize(¤tSize); + } +} diff --git a/plugins/obs-vst3/VST3EditorWindow.h b/plugins/obs-vst3/VST3EditorWindow.h new file mode 100644 index 00000000000000..6a7ea791c41768 --- /dev/null +++ b/plugins/obs-vst3/VST3EditorWindow.h @@ -0,0 +1,89 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once +#include "pluginterfaces/gui/iplugview.h" +#include "pluginterfaces/gui/iplugviewcontentscalesupport.h" + +#include +#include + +#include +#include + +class QCloseEvent; +class QEvent; +class QResizeEvent; +class QTimer; + +class VST3EditorWindow : public QWidget { +public: + VST3EditorWindow(Steinberg::IPlugView *view, const std::string &title); + ~VST3EditorWindow() override; + + VST3EditorWindow(const VST3EditorWindow &) = delete; + VST3EditorWindow &operator=(const VST3EditorWindow &) = delete; + VST3EditorWindow(VST3EditorWindow &&) = delete; + VST3EditorWindow &operator=(VST3EditorWindow &&) = delete; + + bool create(int width, int height); + void show(); + void close(); + // The window is hidden rather than destroyed to preserve its size and position. + bool getClosedState() const; + +protected: + bool event(QEvent *event) override; + void closeEvent(QCloseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; +#ifdef _WIN32 + bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; +#endif +private: + class PlugFrameImpl; + + QSize vst3ToQtSize(const Steinberg::ViewRect &rect) const; + Steinberg::ViewRect qtToVst3Rect(const QSize &size) const; + qreal vst3CoordinateScaleFactor() const; + + Steinberg::tresult PLUGIN_API resizeFromPlugin(const Steinberg::ViewRect &rect); + bool updateContentScaleFactor(); + void handleScaleChange(); + + Steinberg::IPlugView *view_ = nullptr; + std::unique_ptr frame_; + QWidget *viewContainer_ = nullptr; + + bool resizable_ = false; + bool attached_ = false; + bool wasClosed_ = false; + + bool resizingFromPlugin_ = false; + bool correctingHostResize_ = false; + bool resizeViewRecursionGuard_ = false; + + bool handlingDpiChange_ = false; + qreal contentScaleFactor_ = 0.0; + +#ifdef __APPLE__ + void setMacContentAspectRatio(const QSize &size); +#endif +#ifdef __linux__ + void handleLinuxResize(); + QTimer *resizeLinuxTimer_ = nullptr; +#endif +}; diff --git a/plugins/obs-vst3/VST3HostApp.cpp b/plugins/obs-vst3/VST3HostApp.cpp new file mode 100644 index 00000000000000..129065cb0174aa --- /dev/null +++ b/plugins/obs-vst3/VST3HostApp.cpp @@ -0,0 +1,96 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "VST3HostApp.h" + +#include "pluginterfaces/vst/ivstaudioprocessor.h" +#include "pluginterfaces/vst/ivsteditcontroller.h" +#include "pluginterfaces/vst/ivstmessage.h" +#include "public.sdk/source/vst/utility/stringconvert.h" + +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +using namespace Steinberg; +using namespace Vst; + +VST3HostApp::VST3HostApp(VST3Backend backend) : backend_(backend) +{ + addPlugInterfaceSupported(IComponent::iid); + addPlugInterfaceSupported(IAudioProcessor::iid); + addPlugInterfaceSupported(IEditController::iid); + addPlugInterfaceSupported(IConnectionPoint::iid); +} + +VST3HostApp::~VST3HostApp() noexcept {FUNKNOWN_DTOR} + +tresult PLUGIN_API VST3HostApp::getName(String128 name) +{ + return StringConvert::convert("OBS VST3 Host", name) ? kResultTrue : kInternalError; +} + +tresult PLUGIN_API VST3HostApp::createInstance(TUID cid, TUID iid_, void **obj) +{ + if (FUnknownPrivate::iidEqual(cid, IMessage::iid) && FUnknownPrivate::iidEqual(iid_, IMessage::iid)) { + *obj = new HostMessage; + return kResultTrue; + } + if (FUnknownPrivate::iidEqual(cid, IAttributeList::iid) && + FUnknownPrivate::iidEqual(iid_, IAttributeList::iid)) { + if (auto al = HostAttributeList::make()) { + *obj = al.take(); + return kResultTrue; + } + return kOutOfMemory; + } + *obj = nullptr; + return kResultFalse; +} + +tresult PLUGIN_API VST3HostApp::isPlugInterfaceSupported(const TUID iid_) +{ + auto uid = FUID::fromTUID(iid_); + if (std::find(FUIDArray_.begin(), FUIDArray_.end(), uid) != FUIDArray_.end()) { + return kResultTrue; + } + return kResultFalse; +} + +tresult PLUGIN_API VST3HostApp::queryInterface(const TUID iid_, void **obj) +{ + if (FUnknownPrivate::iidEqual(iid_, IHostApplication::iid)) { + *obj = static_cast(this); + return kResultOk; + } + if (FUnknownPrivate::iidEqual(iid_, IPlugInterfaceSupport::iid)) { + *obj = static_cast(this); + return kResultOk; + } +#ifdef __linux__ + if (runLoop && FUnknownPrivate::iidEqual(iid_, Linux::IRunLoop::iid)) { + *obj = static_cast(runLoop); + return kResultOk; + } +#endif + *obj = nullptr; + return kNoInterface; +} diff --git a/plugins/obs-vst3/VST3HostApp.h b/plugins/obs-vst3/VST3HostApp.h new file mode 100644 index 00000000000000..6391c87b1d1030 --- /dev/null +++ b/plugins/obs-vst3/VST3HostApp.h @@ -0,0 +1,73 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once +#ifdef __linux__ +#include "RunLoopImpl.h" +#endif + +#include "public.sdk/source/vst/hosting/hostclasses.h" + +using namespace Steinberg; +using namespace Vst; + +class VST3Plugin; + +enum class VST3Backend { + Unknown, + Windows, + MacOS, + X11, + Wayland, +}; + +class VST3HostApp : public IHostApplication, public IPlugInterfaceSupport { +public: + explicit VST3HostApp(VST3Backend backend); + ~VST3HostApp() noexcept; + + VST3Backend backend() const noexcept { return backend_; } + + VST3HostApp(const VST3HostApp &) = delete; + VST3HostApp &operator=(const VST3HostApp &) = delete; + VST3HostApp(VST3HostApp &&) = delete; + VST3HostApp &operator=(VST3HostApp &&) = delete; + + tresult PLUGIN_API getName(String128 name) override; + tresult PLUGIN_API createInstance(TUID cid, TUID iid_, void **obj) override; + tresult PLUGIN_API isPlugInterfaceSupported(const TUID iid_) override; + + tresult PLUGIN_API queryInterface(const TUID iid_, void **obj) override; + // we do not care here of the ref-counting. A plug-in call of release should not destroy this class ! + uint32 PLUGIN_API addRef() override { return 1000; } + uint32 PLUGIN_API release() override { return 1000; } + + FUnknown *getFUnknown() noexcept { return static_cast(static_cast(this)); } + +#ifdef __linux__ + //========== Pass Runloop ===========// + void setRunLoop(Steinberg::Linux::IRunLoop *rl) { runLoop = rl; } + +private: + Steinberg::Linux::IRunLoop *runLoop = nullptr; +#endif +private: + const VST3Backend backend_; + + std::vector FUIDArray_; + void addPlugInterfaceSupported(const TUID iid_) { FUIDArray_.push_back(FUID::fromTUID(iid_)); } +}; diff --git a/plugins/obs-vst3/VST3Plugin.cpp b/plugins/obs-vst3/VST3Plugin.cpp new file mode 100644 index 00000000000000..77cf957361195d --- /dev/null +++ b/plugins/obs-vst3/VST3Plugin.cpp @@ -0,0 +1,623 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include "VST3EditorWindow.h" +#include "VST3HostApp.h" +#include "obs-vst3.h" + +#include "public.sdk/source/common/memorystream.h" + +#include + +#include +#include + +using namespace Steinberg; +using namespace Steinberg::Vst; + +namespace Steinberg { +const FUID IPlugView::iid(0x5BC32507, 0xD06049EA, 0xA6151B52, 0x2B755B29); +const FUID IPlugViewContentScaleSupport::iid(0x65ED9690, 0x8AC44525, 0x8AADEF7A, 0x72EA703F); +const FUID IPlugFrame::iid(0x367FAF01, 0xAFA94693, 0x8D4DA2A0, 0xED0882A3); +} // namespace Steinberg + +#ifdef __linux__ +/* Hack for LSP plugins which crash with NVIDIA drivers. Switch LSP UI rendering from OpenGL(GLX) to Cairo (software). */ +static void ensure_lsp_cairo_backend() +{ + setenv("LSP_WS_LIB_GLXSURFACE", "off", 1); + infovst3plugin("Workaround for LSP plugins on linux; LSP_WS_LIB_GLXSURFACE=off set (forcing Cairo backend)\n"); +} +#endif + +VST3Plugin::VST3Plugin() +{ + if (!hostContext) { + hostContext = get_host_app(); + } + + componentContext = new VST3ComponentHolder(this); + +#ifdef __linux__ + ensure_lsp_cairo_backend(); +#endif +}; + +VST3Plugin::~VST3Plugin() +{ + if (window) { + hideEditor(); + + if (view) { + view->removed(); + view->setFrame(nullptr); + } + + delete window; + window = nullptr; + } + + view = nullptr; + + processData.unprepare(); + + plugProvider = nullptr; + module = nullptr; + + delete componentContext; + componentContext = nullptr; +} + +void VST3Plugin::deactivateComponent() const +{ + vstPlug->setActive(false); +} + +void VST3Plugin::setBusActive(MediaType type, BusDirection direction, int which, bool active) const +{ + vstPlug->activateBus(type, direction, which, active); +} + +bool VST3Plugin::scanAudioBuses(SpeakerArrangement arr) +{ + int auxBusCount = 0; + int mainBusCount = 0; + numInputAudioBuses = vstPlug->getBusCount(MediaTypes::kAudio, BusDirections::kInput); + numOutputAudioBuses = vstPlug->getBusCount(MediaTypes::kAudio, BusDirections::kOutput); + + infovst3plugin("Input audio buses: %i\n Output audio buses: %i\n", numInputAudioBuses, numOutputAudioBuses); + + inputAudioBusInfos.clear(); + inputSpeakerArrangements.clear(); + outputAudioBusInfos.clear(); + outputSpeakerArrangements.clear(); + // We enable the 1st compatible Main bus and the 1st Aux bus + for (int i = 0; i < numInputAudioBuses; ++i) { + BusInfo info = {}; + vstPlug->getBusInfo(kAudio, kInput, i, info); + inputAudioBusInfos.push_back(info); + // only 1 Main input bus is enabled by obs + 1 Aux (side-channel) Bus if it is available + if (info.busType == Steinberg::Vst::BusTypes::kMain) { + if (mainBusCount == 0) { + setBusActive(kAudio, kInput, i, true); + mainInputBusNumChannels = info.channelCount; + mainInputBusIndex = i; + inputSpeakerArrangements.push_back(arr); + numEnabledInputAudioBuses++; + mainBusCount = 1; + } else { + setBusActive(kAudio, kInput, i, false); + } + } else { + // The 1st aux bus (sidechain) is enabled only if it is mono or stereo + if (auxBusCount == 0 && (info.channelCount == 1 || info.channelCount == 2)) { + setBusActive(kAudio, kInput, i, true); + SpeakerArrangement speakerArr = info.channelCount == 1 + ? Steinberg::Vst::SpeakerArr::kMono + : Steinberg::Vst::SpeakerArr::kStereo; + inputSpeakerArrangements.push_back(speakerArr); + numEnabledInputAudioBuses++; + sidechainNumChannels = info.channelCount; + auxBusIndex = i; + auxBusCount = 1; + } else { + setBusActive(kAudio, kInput, i, false); + } + } + } + // We disable the plugin if it has no Input bus. + if (!numEnabledInputAudioBuses) { + infovst3plugin( + "No input bus detected ! OBS VST3 Host only supports audio effects VST3 with 1 Main Input Bus (+ 1 Sidechannel Bus)."); + vstPlug->setActive(false); + return false; + } + // Only the 1st Main output bus is enabled + for (int i = 0; i < numOutputAudioBuses; ++i) { + BusInfo info = {}; + vstPlug->getBusInfo(kAudio, kOutput, i, info); + outputAudioBusInfos.push_back(info); + bool isMain = (info.busType == Steinberg::Vst::BusTypes::kMain); + if (isMain && !numEnabledOutputAudioBuses) { + setBusActive(kAudio, kOutput, i, isMain); + mainOutputBusIndex = i; + mainOutputBusNumChannels = info.channelCount; + outputSpeakerArrangements.push_back(arr); + numEnabledOutputAudioBuses++; + return true; + } + } + return false; +} + +bool VST3Plugin::init(const std::string &classId, const std::string &path_, int sample_rate, int max_blocksize, + SpeakerArrangement arrangement) +{ + std::string error; + + path = path_; + + sampleRate = sample_rate; + maxBlockSize = max_blocksize; + symbolicSampleSize = kSample32; + realtime = kRealtime; + + processSetup.processMode = realtime; + processSetup.symbolicSampleSize = symbolicSampleSize; + processSetup.sampleRate = sampleRate; + processSetup.maxSamplesPerBlock = maxBlockSize; + + processContext.state = ProcessContext::kPlaying | ProcessContext::kRecording | ProcessContext::kSystemTimeValid; + processContext.sampleRate = sampleRate; + + processData.numSamples = 0; + processData.symbolicSampleSize = symbolicSampleSize; + processData.processContext = &processContext; + + module = VST3::Hosting::Module::create(path, error); + if (!module) { + infovst3plugin("%s", error.c_str()); + return false; + } + + componentContext->guiToDsp = &guiToDsp; + inputParameterChanges = std::make_unique(); + outputParameterChanges = std::make_unique(); + + VST3::Hosting::PluginFactory factory = module->getFactory(); + factory.setHostContext(hostContext->getFUnknown()); + + for (auto &classInfo : factory.classInfos()) { + if (classInfo.category() == kVstAudioEffectClass && classInfo.ID().toString() == classId) { + if (classId != classInfo.ID().toString()) { + continue; + } + plugProvider = owned(new OBSPlugProvider(factory, classInfo, false)); + if (plugProvider->setup(hostContext->getFUnknown()) == false) { + plugProvider = nullptr; + } + name = classInfo.name(); + break; + } + } + if (!plugProvider) { + infovst3plugin("No VST3 Audio Module Class with UID %s found. You probably uninstalled the VST3.", + classId.c_str()); + return false; + } + + vstPlug = plugProvider->getComponentPtr(); + if (!vstPlug) { + infovst3plugin("No VST3 Component class found."); + return false; + } + + editController = plugProvider->getControllerPtr(); + if (!editController) { + infovst3plugin("No VST3 EditorController class found."); + return false; + } else { + editController->setComponentHandler(componentContext->getComponentHandler()); + } + + const int32 paramCount = editController ? editController->getParameterCount() : 0; + guiToDsp.setMaxParameters(paramCount > 0 ? paramCount : 256); + dspToGui.setMaxParameters(paramCount > 0 ? paramCount : 256); + + audioEffect = FUnknownPtr(vstPlug).getInterface(); + if (!audioEffect) { + infovst3plugin("Failed to get an audio processor from VST3"); + // try to get audioProcessor from EditorController, à la Juce, from badly coded VST3. + audioEffect = FUnknownPtr(editController).getInterface(); + if (!audioEffect) { + return false; + } + } + + if (!scanAudioBuses(arrangement)) { + obsVst3Data->bypass.store(true, std::memory_order_relaxed); + infovst3plugin("Error during the bus scan."); + return false; + } + + // Some plug-ins will crash if we pass a nullptr to setBusArrangements! + SpeakerArrangement nullArrangement = {}; + auto *inputArrangements = inputSpeakerArrangements.empty() ? &nullArrangement : inputSpeakerArrangements.data(); + auto *outputArrangements = outputSpeakerArrangements.empty() ? &nullArrangement + : outputSpeakerArrangements.data(); + tresult res = audioEffect->setBusArrangements(inputArrangements, numEnabledInputAudioBuses, outputArrangements, + numEnabledOutputAudioBuses); + if (res != kResultTrue) { + SpeakerArrangement speakerArrangement; + audioEffect->getBusArrangement(kInput, mainInputBusIndex, speakerArrangement); + if (speakerArrangement != arrangement) { + infovst3plugin("Failed to set input bus to obs speaker layout."); + return false; + } + + if (numEnabledInputAudioBuses == 2) { + audioEffect->getBusArrangement(kInput, auxBusIndex, speakerArrangement); + SpeakerArrangement sideArr = sidechainNumChannels == 1 ? Steinberg::Vst::SpeakerArr::kMono + : Steinberg::Vst::SpeakerArr::kStereo; + if (speakerArrangement != sideArr) { + infovst3plugin("Failed to set side chain bus to desired speaker layout!"); + return false; + } + } + + audioEffect->getBusArrangement(kOutput, mainOutputBusIndex, speakerArrangement); + if (speakerArrangement != arrangement) { + infovst3plugin("Failed to set output bus to obs speaker layout."); + return false; + } + } + + res = audioEffect->setupProcessing(processSetup); + if (res == kResultOk) { + processData.prepare(*vstPlug, maxBlockSize, processSetup.symbolicSampleSize); + // silence outputs on preparation, better safe than sorry + for (int32 busIdx = 0; busIdx < processData.numOutputs; ++busIdx) { + auto &bus = processData.outputs[busIdx]; + + if (bus.channelBuffers32) { + for (int32 ch = 0; ch < bus.numChannels; ++ch) { + std::fill_n(bus.channelBuffers32[ch], maxBlockSize, 0.0f); + } + } + } + } else { + infovst3plugin("Failed to setup VST3 processing."); + return false; + } + + if (vstPlug->setActive(true) != kResultTrue) { + infovst3plugin("Failed to activate VST3 component."); + return false; + } + // this often reports 0 in my tests, which probably means that the VST3 authors didn't really measure the value, lol + uint32 latency = audioEffect->getLatencySamples(); + infovst3plugin("Latency of the plugin is %i samples", latency); + + return true; +} + +void VST3Plugin::drainDspToGui() +{ + ParamID id; + ParamValue value; + int32 sampleOffset; + + while (dspToGui.getNextChange(id, value, sampleOffset)) { + if (editController) { + editController->setParamNormalized(id, value); + } + } + uiDrainScheduled.store(false, std::memory_order_release); +} + +void VST3Plugin::preprocess() +{ + inputParameterChanges->clearQueue(); + outputParameterChanges->clearQueue(); + processData.inputParameterChanges = inputParameterChanges.get(); + processData.outputParameterChanges = outputParameterChanges.get(); + guiToDsp.transferChangesTo(*inputParameterChanges); +} + +void VST3Plugin::postprocess() +{ + if (!processData.outputParameterChanges || outputParameterChanges->getParameterCount() == 0) { + return; + } + + dspToGui.transferChangesFrom(*outputParameterChanges); + + if (!uiDrainScheduled.exchange(true, std::memory_order_acq_rel)) { + QObject *receiver = QCoreApplication::instance(); + if (receiver) { + QPointer self(this); + QMetaObject::invokeMethod( + receiver, + [self] { + if (self) { + self->drainDspToGui(); + } + }, + Qt::QueuedConnection); + } else { + uiDrainScheduled.store(false, std::memory_order_release); + } + } +} + +void VST3Plugin::setProcessing(bool processing) const +{ + audioEffect->setProcessing(processing); +} + +bool VST3Plugin::process(int numSamples) +{ + if (!audioEffect) { + return false; + } + + preprocess(); + + if (numSamples > maxBlockSize) { +#ifdef _DEBUG + infovst3plugin("numSamples > _maxBlockSize"); +#endif + numSamples = maxBlockSize; + } + + processData.numSamples = numSamples; + processContext.projectTimeSamples += numSamples; + processContext.systemTime = static_cast(os_gettime_ns()); + + tresult result = audioEffect->process(processData); + + if (result != kResultOk) { + return false; + } + + postprocess(); + + return true; +} + +Steinberg::Vst::Sample32 *VST3Plugin::channelBuffer32(const BusDirection direction, const int ch) const +{ + if (direction == kInput) { + return processData.inputs[mainInputBusIndex].channelBuffers32[ch]; + } else if (direction == kOutput) { + return processData.outputs[mainOutputBusIndex].channelBuffers32[ch]; + } else { + return nullptr; + } +} + +Steinberg::Vst::Sample32 *VST3Plugin::auxChannelBuffer32(const BusDirection direction, const int ch) const +{ + if (direction == kInput) { + return processData.inputs[auxBusIndex].channelBuffers32[ch]; + } else { + return nullptr; + } +} + +/* hack ripped from Juce, to create the view even with badly coded VST3s... */ +void VST3Plugin::tryCreatingView() +{ + if (auto *raw = editController->createView(Vst::ViewType::kEditor)) { + view = IPtr::adopt(raw); + return; + } + + if (auto *raw = editController->createView(nullptr)) { + view = IPtr::adopt(raw); + return; + } + + IPlugView *raw = nullptr; + if (editController->queryInterface(IPlugView::iid, reinterpret_cast(&raw)) == kResultOk) { + view = IPtr::adopt(raw); + } +} + +bool VST3Plugin::createView() +{ + if (!editController) { + infovst3plugin("VST3 does not provide an edit controller"); + return false; + } + + if (view) { + debugvst3plugin("Editor view or window already exists"); + return false; + } else { + tryCreatingView(); + } + + if (!view) { + infovst3plugin("EditController does not provide its own view"); + return false; + } + +#ifdef _WIN32 + if (view->isPlatformTypeSupported(Steinberg::kPlatformTypeHWND) != Steinberg::kResultTrue) { + infovst3plugin("Editor view does not support HWND"); + return false; + } +#elif defined(__APPLE__) + if (view->isPlatformTypeSupported(Steinberg::kPlatformTypeNSView) != Steinberg::kResultTrue) { + infovst3plugin("Editor view does not support NSView"); + return false; + } +#elif defined(__linux__) + if (hostContext->backend() == VST3Backend::X11 && + view->isPlatformTypeSupported(Steinberg::kPlatformTypeX11EmbedWindowID) != Steinberg::kResultTrue) { + infovst3plugin("Editor view does not support X11"); + return false; + } + if (hostContext->backend() == VST3Backend::Wayland) { + infovst3plugin( + "Our VST3 host does not support Wayland for GUI. VST3s will be active in headless mode."); + return false; + } +#else + infovst3plugin("Platform is not supported yet"); + return false; +#endif + + return true; +} + +void VST3Plugin::showEditor() +{ + if (!view) { + return; + } + + if (!window) { + int width = 800, height = 600; + Steinberg::ViewRect rect; + if (view->getSize(&rect) == Steinberg::kResultOk) { + width = rect.getWidth(); + height = rect.getHeight(); + } else { + infovst3plugin("Failed to get size before attaching an IFrame. Not SDK compliant."); + } + std::string sourceName = obs_source_get_name(obsVst3Data->context); + std::string windowName = sourceName + ": VST3 Plugin - " + name; + + window = new VST3EditorWindow(view, windowName); + + if (window->create(width, height)) { + window->show(); + editorVisible = true; + } else { + infovst3plugin("Failed to create editor window"); + delete window; + window = nullptr; + } + } else { + window->show(); + editorVisible = true; + } +} + +void VST3Plugin::hideEditor() +{ + if (window && view) { + window->close(); + } + editorVisible = false; +} + +// This function is required because we don't really close the GUI window; we hide it. This then means we have to track +// when a GUI has been closed by the user when clicking X. I decided to just hide because creating the GUI each time +// the user wants to display it, was prone to crashes. This also simplified the coding. +bool VST3Plugin::isEditorVisible() +{ + if (window) { + bool wasClosed = window->getClosedState(); + if (wasClosed && editorVisible) { + editorVisible = false; + } + } + return editorVisible; +} + +bool VST3Plugin::saveStates(std::vector &compOut, std::vector &ctrlOut) const +{ + compOut.clear(); + ctrlOut.clear(); + + if (!vstPlug) { + return false; + } + + { + Steinberg::MemoryStream s; + if (vstPlug->getState(&s) != Steinberg::kResultOk) { + return false; + } + + Steinberg::int64 size = 0, seekRes = 0; + s.tell(&size); + if (size <= 0) { + return false; + } + + compOut.resize(static_cast(size)); + s.seek(0, Steinberg::IBStream::kIBSeekSet, &seekRes); + Steinberg::int32 actuallyRead = 0; + s.read(compOut.data(), static_cast(size), &actuallyRead); + if (actuallyRead < size) { + compOut.resize(static_cast(actuallyRead)); + } + } + + if (editController) { + Steinberg::MemoryStream s; + if (editController->getState(&s) == Steinberg::kResultOk) { + Steinberg::int64 size = 0, seekRes = 0; + s.tell(&size); + if (size > 0) { + ctrlOut.resize(static_cast(size)); + s.seek(0, Steinberg::IBStream::kIBSeekSet, &seekRes); + Steinberg::int32 actuallyRead = 0; + s.read(ctrlOut.data(), static_cast(size), &actuallyRead); + if (actuallyRead < size) { + ctrlOut.resize(static_cast(actuallyRead)); + } + } + } + } + + return true; +} + +bool VST3Plugin::loadStates(const std::vector &comp, const std::vector &ctrl) const +{ + if (!vstPlug || comp.empty()) { + return false; + } + + Steinberg::MemoryStream compStream; + Steinberg::int32 w = 0; + Steinberg::int64 dummy = 0; + + compStream.write((void *)comp.data(), static_cast(comp.size()), &w); + compStream.seek(0, Steinberg::IBStream::kIBSeekSet, &dummy); + if (vstPlug->setState(&compStream) != Steinberg::kResultOk) { + return false; + } + + if (editController) { + compStream.seek(0, Steinberg::IBStream::kIBSeekSet, &dummy); + (void)editController->setComponentState(&compStream); + if (!ctrl.empty()) { + Steinberg::MemoryStream ctrlStream; + ctrlStream.write((void *)ctrl.data(), static_cast(ctrl.size()), &w); + ctrlStream.seek(0, Steinberg::IBStream::kIBSeekSet, &dummy); + (void)editController->setState(&ctrlStream); + } + } + return true; +} diff --git a/plugins/obs-vst3/VST3Plugin.h b/plugins/obs-vst3/VST3Plugin.h new file mode 100644 index 00000000000000..e5dd5953b5a1c5 --- /dev/null +++ b/plugins/obs-vst3/VST3Plugin.h @@ -0,0 +1,133 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + Portions are derived from EasyVst (https://github.com/iffyloop/EasyVst), + licensed under Public Domain (Unlicense) or MIT No Attribution. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "VST3ComponentHolder.h" + +#include "pluginterfaces/base/smartpointer.h" +#include "pluginterfaces/gui/iplugview.h" +#include "pluginterfaces/vst/ivstprocesscontext.h" + +#include "public.sdk/source/vst/hosting/module.h" +#include "public.sdk/source/vst/hosting/parameterchanges.h" +#include "public.sdk/source/vst/hosting/plugprovider.h" +#include "public.sdk/source/vst/hosting/processdata.h" + +#include + +#define do_logVST3(level, format, ...) \ + blog(level, "[VST3 Plugin:] " format, ## __VA_ARGS__) + +#define warnvst3plugin(format, ...) do_logVST3(LOG_WARNING, format, ## __VA_ARGS__) +#define infovst3plugin(format, ...) do_logVST3(LOG_INFO, format, ## __VA_ARGS__) +#define debugvst3plugin(format, ...) do_logVST3(LOG_DEBUG, format, ## __VA_ARGS__) + +using namespace Steinberg; +using namespace Vst; + +class VST3EditorWindow; +class VST3ComponentHolder; +class VST3HostApp; +struct vst3_audio_data; + +class OBSPlugProvider : public PlugProvider { +public: + using PlugProvider::PlugProvider; + bool setup(FUnknown *context) { return setupPlugin(context); } +}; + +class VST3Plugin : public QObject { + Q_OBJECT +public: + VST3Plugin(); + ~VST3Plugin(); + + VST3Plugin(const VST3Plugin &) = delete; + VST3Plugin &operator=(const VST3Plugin &) = delete; + VST3Plugin(VST3Plugin &&) = delete; + VST3Plugin &operator=(VST3Plugin &&) = delete; + + struct vst3_audio_data *obsVst3Data = nullptr; + + VST3HostApp *hostContext = nullptr; + VST3ComponentHolder *componentContext = nullptr; + VST3::Hosting::Module::Ptr module = nullptr; + IPtr plugProvider = nullptr; + IComponent *vstPlug = nullptr; + IAudioProcessor *audioEffect = nullptr; + IEditController *editController = nullptr; + HostProcessData processData = {}; + ProcessSetup processSetup = {}; + ProcessContext processContext = {}; + + IPtr view = nullptr; + + bool loadStates(const std::vector &comp, const std::vector &ctrl) const; + bool saveStates(std::vector &compOut, std::vector &ctrlOut) const; + + bool scanAudioBuses(SpeakerArrangement arr); + void setBusActive(MediaType type, BusDirection direction, int which, bool active) const; + bool init(const std::string &classId, const std::string &path, int sampleRate, int maxBlockSize, + SpeakerArrangement arr); + void deactivateComponent() const; + + void setProcessing(bool processing) const; + bool process(int numSamples); + void preprocess(); + void postprocess(); + [[nodiscard]] Sample32 *channelBuffer32(BusDirection direction, int which) const; + [[nodiscard]] Sample32 *auxChannelBuffer32(BusDirection direction, int ch) const; + + void tryCreatingView(); + bool createView(); + void showEditor(); + void hideEditor(); + VST3EditorWindow *window = nullptr; + bool editorVisible = false; + bool isEditorVisible(); + + ParameterChangeTransfer guiToDsp; + ParameterChangeTransfer dspToGui; + std::unique_ptr inputParameterChanges; + std::unique_ptr outputParameterChanges; + std::atomic uiDrainScheduled{false}; + void drainDspToGui(); + + int sampleRate = 0; + int maxBlockSize = 0; + int symbolicSampleSize = 0; + bool realtime = kRealtime; + std::vector inputAudioBusInfos, outputAudioBusInfos; + int numInputAudioBuses = 0; + int numOutputAudioBuses = 0; + int numEnabledInputAudioBuses = 0; + int numEnabledOutputAudioBuses = 0; + int mainInputBusNumChannels = 0; + int mainOutputBusNumChannels = 0; + int sidechainNumChannels = 0; + int mainInputBusIndex = 0; + int mainOutputBusIndex = 0; + int auxBusIndex = 0; + std::vector inputSpeakerArrangements, outputSpeakerArrangements; + + std::string path; + std::string name; +}; diff --git a/plugins/obs-vst3/VST3Scanner.cpp b/plugins/obs-vst3/VST3Scanner.cpp new file mode 100644 index 00000000000000..c64561a4a712d0 --- /dev/null +++ b/plugins/obs-vst3/VST3Scanner.cpp @@ -0,0 +1,333 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "VST3Scanner.h" + +#include "public.sdk/source/vst/hosting/module.h" +#include "public.sdk/source/vst/moduleinfo/moduleinfoparser.h" + +#include "util/bmem.h" +#include + +#include +#include +#include +#include + +std::vector VST3Scanner::getDefaultSearchPaths() +{ + std::vector paths; + +#ifdef _WIN32 + char *programFiles = std::getenv("ProgramFiles"); + if (programFiles) { + paths.emplace_back(std::string(programFiles) + "\\Common Files\\VST3"); + } + + char *localAppData = std::getenv("LOCALAPPDATA"); + if (localAppData) { + paths.emplace_back(std::string(localAppData) + "\\Programs\\Common\\VST3"); + } +#elif defined(__APPLE__) + paths.emplace_back("/Library/Audio/Plug-Ins/VST3"); + if (const char *home = std::getenv("HOME")) { + paths.emplace_back(std::string(home) + "/Library/Audio/Plug-Ins/VST3"); + } +#elif defined(__linux__) + paths.emplace_back("/usr/lib/vst3"); + paths.emplace_back("/usr/local/lib/vst3"); + if (const char *home = std::getenv("HOME")) { + paths.emplace_back(std::string(home) + "/.vst3"); + } +#endif + return paths; +} + +std::unordered_set VST3Scanner::getVST3Paths() +{ + std::unordered_set fsPaths; + + for (const auto &folder : getDefaultSearchPaths()) { + if (!std::filesystem::exists(folder)) { + continue; + } + + for (const auto &entry : std::filesystem::recursive_directory_iterator(folder)) { + if (!entry.exists()) { + continue; + } + +#ifdef _WIN32 + if (entry.is_regular_file() && entry.path().extension() == ".vst3") +#else + if (entry.is_directory() && entry.path().extension() == ".vst3") +#endif + fsPaths.insert(entry.path().string()); + } + } + + return fsPaths; +} + +static std::string lowerAscii(std::string s) noexcept +{ + for (auto &c : s) { + if (c >= 'A' && c <= 'Z') { + c = static_cast(c - 'A' + 'a'); + } + } + return s; +} + +void VST3Scanner::sort() +{ + std::sort(pluginList.begin(), pluginList.end(), [](const VST3ClassInfo &a, const VST3ClassInfo &b) { + const auto an = lowerAscii(a.name); + const auto bn = lowerAscii(b.name); + if (an != bn) { + return an < bn; + } + + const auto ap = lowerAscii(a.pluginName); + const auto bp = lowerAscii(b.pluginName); + if (ap != bp) { + return ap < bp; + } + + if (a.path != b.path) { + return a.path < b.path; + } + + return a.id < b.id; + }); +} + +bool VST3Scanner::hasVST3() +{ + auto paths = getDefaultSearchPaths(); + + for (const auto &folder : paths) { + if (!std::filesystem::exists(folder)) { + continue; + } + + try { + for (const auto &entry : std::filesystem::directory_iterator(folder)) { + if (!entry.exists()) { + continue; + } + + const auto &p = entry.path(); + +#ifdef _WIN32 + if (entry.is_regular_file() && p.extension() == ".vst3") { + return true; + } +#elif defined(__APPLE__) || defined(__linux__) + if (entry.is_directory() && p.extension() == ".vst3") { + return true; + } +#endif + } + } catch (const std::exception &e) { + (void)e; + } + } + + return false; +} + +static bool isInstrument(const std::vector &subCategories) +{ + for (const auto &category : subCategories) { + if (category.find("Instrument") != std::string::npos) { + return true; + } + } + + return false; +} + +bool VST3Scanner::scanForVST3Plugins() +{ + pluginList.clear(); + classCount.clear(); + auto paths = getVST3Paths(); + + for (const auto &bundlePath : paths) { + if (!tryReadModuleInfo(bundlePath)) { + addModuleClasses(bundlePath); + } + } + sort(); + return !pluginList.empty(); +} + +void VST3Scanner::updateModulesList(std::unordered_map &modules) +{ + std::unordered_set fsPaths = getVST3Paths(); + + for (auto it = modules.begin(); it != modules.end();) { + if (!fsPaths.count(it->first)) { + it = modules.erase(it); + } else { + ++it; + } + } + + for (const auto &path : fsPaths) { + if (!modules.count(path)) { + if (!tryReadModuleInfo(path)) { + addModuleClasses(path); + } + } + } +} + +bool VST3Scanner::tryReadModuleInfo(const std::string &bundlePath) +{ + namespace fs = std::filesystem; + + fs::path p(bundlePath); + fs::path bundleRoot; + +#ifdef _WIN32 + if (!fs::is_regular_file(p)) { + return false; + } + + bundleRoot = p.parent_path().parent_path().parent_path(); +#else + if (!fs::is_directory(p)) { + return false; + } + + bundleRoot = p; +#endif + + fs::path jsonPath = bundleRoot / "Contents" / "Resources" / "moduleinfo.json"; + + if (!fs::exists(jsonPath) || !fs::is_regular_file(jsonPath)) { + return false; + } + + char *jsonBuf = os_quick_read_utf8_file(jsonPath.string().c_str()); + if (!jsonBuf) { + return false; + } + + std::string json(jsonBuf); + bfree(jsonBuf); + + auto parsed = Steinberg::ModuleInfoLib::parseJson(json, nullptr); + if (!parsed) { + return false; + } + + bool discardable = parsed.value().factoryInfo.flags & Steinberg::PFactoryInfo::kClassesDiscardable; + + if (discardable) { + return addModuleClasses(bundlePath); + } + + return loadFromModuleInfo(*parsed, bundleRoot.string()); +} + +bool VST3Scanner::loadFromModuleInfo(const Steinberg::ModuleInfo &info, const std::string &bundleRoot) +{ + const std::string pluginName = std::filesystem::path(bundleRoot).stem().string(); + size_t added = 0; + bool discardable = info.factoryInfo.flags & Steinberg::PFactoryInfo::kClassesDiscardable; + for (const auto &c : info.classes) { + if (c.category != kVstAudioEffectClass || isInstrument(c.subCategories)) { + continue; + } + + VST3ClassInfo entry; + entry.id = c.cid; + entry.name = c.name; + entry.path = bundleRoot; + entry.pluginName = pluginName; + entry.discardable = discardable; + + pluginList.push_back(std::move(entry)); + ++classCount[bundleRoot]; + ++added; + } + + return added > 0; +} + +bool VST3Scanner::addModuleClasses(const std::string &bundlePath) +{ + std::string error; + const std::string pluginName = std::filesystem::path(bundlePath).stem().string(); + size_t added = 0; + VST3::Hosting::Module::Ptr module = VST3::Hosting::Module::create(bundlePath, error); + + if (!module) { + blog(LOG_ERROR, "[VST3 Scanner] Module failed to load with error %s", error.c_str()); + return false; + } + + VST3::Hosting::PluginFactory factory = module->getFactory(); + bool discardable = factory.info().classesDiscardable(); + for (const auto &classInfo : factory.classInfos()) { + if (classInfo.category() != kVstAudioEffectClass || isInstrument(classInfo.subCategories())) { + continue; + } + + VST3ClassInfo entry; + entry.id = classInfo.ID().toString(); + entry.name = classInfo.name(); + entry.pluginName = pluginName; + entry.path = bundlePath; + entry.discardable = discardable; + + pluginList.push_back(std::move(entry)); + ++classCount[bundlePath]; + ++added; + } + + return added > 0; +} + +bool VST3Scanner::moduleHasMultipleClasses(const std::string &bundlePath) const +{ + auto it = classCount.find(bundlePath); + return it != classCount.end() && it->second > 1; +} + +std::string VST3Scanner::getNameById(const std::string &class_id) const +{ + for (const auto &c : pluginList) { + if (c.id == class_id) { + return c.name; + } + } + return {}; +} + +std::string VST3Scanner::getPathById(const std::string &class_id) const +{ + for (const auto &c : pluginList) { + if (c.id == class_id) { + return c.path; + } + } + return {}; +} diff --git a/plugins/obs-vst3/VST3Scanner.h b/plugins/obs-vst3/VST3Scanner.h new file mode 100644 index 00000000000000..b8dbff70f5a58c --- /dev/null +++ b/plugins/obs-vst3/VST3Scanner.h @@ -0,0 +1,62 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include "public.sdk/source/vst/moduleinfo/moduleinfo.h" + +#include +#include +#include +#include + +#ifndef kVstAudioEffectClass +#define kVstAudioEffectClass "Audio Module Class" +#endif + +struct VST3ClassInfo { + std::string name; + std::string id; + std::string path; + std::string pluginName; + bool discardable; // Optional but requested to us by Steinberg: the classes need to be reloaded from the module at each host startup and can not be cached (ex: Waves plugins). +}; + +struct ModuleCache { + bool discardable = false; + std::vector classes; +}; + +class VST3Scanner { +public: + std::vector getDefaultSearchPaths(); + std::string getNameById(const std::string &class_id) const; + std::string getPathById(const std::string &class_id) const; + bool moduleHasMultipleClasses(const std::string &bundlePath) const; + bool hasVST3(); + bool addModuleClasses(const std::string &bundlePath); + bool scanForVST3Plugins(); + std::vector pluginList; + void sort(); + std::unordered_map classCount; + void updateModulesList(std::unordered_map &modules); + +private: + std::unordered_set getVST3Paths(); + bool tryReadModuleInfo(const std::string &bundlePath); + bool loadFromModuleInfo(const Steinberg::ModuleInfo &info, const std::string &bundlePath); +}; diff --git a/plugins/obs-vst3/cmake/windows/obs-module.rc.in b/plugins/obs-vst3/cmake/windows/obs-module.rc.in new file mode 100644 index 00000000000000..538bab872d2ce2 --- /dev/null +++ b/plugins/obs-vst3/cmake/windows/obs-module.rc.in @@ -0,0 +1,27 @@ +#define IDI_OBSICON 101 +IDI_OBSICON ICON "obs-studio.ico" + +1 VERSIONINFO +FILEVERSION ${OBS_VERSION_MAJOR},${OBS_VERSION_MINOR},${OBS_VERSION_PATCH},0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "${OBS_COMPANY_NAME}" + VALUE "FileDescription", "VST3 filters" + VALUE "FileVersion", "${OBS_VERSION_CANONICAL}" + VALUE "ProductName", "${OBS_PRODUCT_NAME}" + VALUE "ProductVersion", "${OBS_VERSION_CANONICAL}" + VALUE "Comments", "${OBS_COMMENTS}" + VALUE "LegalCopyright", "${OBS_LEGAL_COPYRIGHT}" + VALUE "InternalName", "obs-vst3" + VALUE "OriginalFilename", "obs-vst3" + END + END + + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END diff --git a/plugins/obs-vst3/cmake/windows/obs-studio.ico b/plugins/obs-vst3/cmake/windows/obs-studio.ico new file mode 100644 index 0000000000000000000000000000000000000000..7127372917d0ae308412302998833d60e420fd4f GIT binary patch literal 93455 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

C#Vyy0Xt+}qRZF9tgd$_hM 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 + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include "obs-vst3.h" + +#include "VST3HostApp.h" +#include "VST3Plugin.h" +#include "VST3Scanner.h" +#ifdef __linux__ +#include "RunLoopImpl.h" + +#include +#endif +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#define MT_ obs_module_text +#define S_PLUGIN "vst3_plugin" +#define S_EDITOR "vst3_open_gui" +#define S_SIDECHAIN_SOURCE "sidechain_source" +#define S_NOGUI "vst3_noview" +#define S_ERR "vst3_error" +#define S_SCAN "vst3_scan" + +#define TEXT_EDITOR MT_("VST3.Button") +#define TEXT_PLUGIN MT_("VST3.Plugin") +#define TEXT_SIDECHAIN_SOURCE MT_("VST3.SidechainSource") +#define TEXT_NOGUI MT_("VST3.NOGUI") +#define TEXT_ERR MT_("VST3.Init.Fail") +#define TEXT_SCAN MT_("VST3.Scan.Ongoing") + +// -------------------------------------------------------- +#define do_log(level, format, ...) \ + blog(level, "[VST3 filter ('%s')]: " format, obs_source_get_name(vd->context), ## __VA_ARGS__) + +#define warnvst3(format, ...) do_log(LOG_WARNING, format, ## __VA_ARGS__) +#define infovst3(format, ...) do_log(LOG_INFO, format, ## __VA_ARGS__) + +#ifdef _DEBUG +#define debugvst3(format, ...) do_log(LOG_DEBUG, format, ## __VA_ARGS__) +#endif +// -------------------------------------------------------- + +struct vst3_audio_info { + uint32_t frames; + uint64_t timestamp; +}; + +struct sidechain_prop_info { + obs_property_t *sources; + obs_source_t *parent; +}; +// ----------------- global host & runloop ---------------- +namespace { +std::unique_ptr g_host_app; +#ifdef __linux__ +std::unique_ptr g_run_loop; +#endif +} // namespace + +VST3HostApp *get_host_app() noexcept +{ + return g_host_app.get(); +} + +bool load_host() +{ + VST3Backend backend = VST3Backend::Unknown; + +#ifdef _WIN32 + backend = VST3Backend::Windows; +#elif defined(__APPLE__) + backend = VST3Backend::MacOS; +#elif defined(__linux__) + const auto platform = obs_get_nix_platform(); + + if (platform == OBS_NIX_PLATFORM_X11_EGL) { + backend = VST3Backend::X11; + } else if (platform == OBS_NIX_PLATFORM_WAYLAND) { + backend = VST3Backend::Wayland; + } +#endif + + if (backend == VST3Backend::Unknown) { + blog(LOG_WARNING, "[VST3 Host] Unsupported platform"); + return false; + } + + auto hostApp = std::make_unique(backend); + +#ifdef __linux__ + auto runLoop = std::make_unique(); + hostApp->setRunLoop(runLoop.get()); +#endif + + g_host_app = std::move(hostApp); + +#ifdef __linux__ + g_run_loop = std::move(runLoop); +#endif + + return true; +} + +void unload_host() +{ +#ifdef __linux__ + if (g_host_app) { + g_host_app->setRunLoop(nullptr); + } + + if (g_run_loop) { + g_run_loop->stop(); + } + + g_run_loop.reset(); +#endif + + g_host_app.reset(); +} +// -------------------- initial scanning ------------------ +VST3Scanner *list; +std::atomic vst3_scan_done; + +static void vst3_cache_save() +{ + if (!list) { + return; + } + + char *path = obs_module_config_path(nullptr); + os_mkdirs(path); + bfree(path); + + char *filepath = obs_module_config_path("vst3list.json"); + + obs_data_t *root = obs_data_create(); + obs_data_array_t *arr = obs_data_array_create(); + + for (const auto &p : list->pluginList) { + obs_data_t *obj = obs_data_create(); + obs_data_set_string(obj, "name", p.name.c_str()); + obs_data_set_string(obj, "id", p.id.c_str()); + obs_data_set_string(obj, "path", p.path.c_str()); + obs_data_set_string(obj, "pluginName", p.pluginName.c_str()); + obs_data_set_bool(obj, "discardable", p.discardable); + obs_data_array_push_back(arr, obj); + obs_data_release(obj); + } + + obs_data_set_int(root, "version", 1); + obs_data_set_array(root, "plugins", arr); + obs_data_array_release(arr); + + obs_data_save_json_safe(root, filepath, "tmp", "bak"); + obs_data_release(root); + bfree(filepath); +} + +static bool vst3_cache_load() +{ + char *path = obs_module_config_path("vst3list.json"); + if (!path) { + return false; + } + + obs_data_t *root = obs_data_create_from_json_file_safe(path, "bak"); + bfree(path); + if (!root) { + return false; + } + + obs_data_array_t *arr = obs_data_get_array(root, "plugins"); + if (!arr) { + obs_data_release(root); + return false; + } + + list->pluginList.clear(); + list->classCount.clear(); + + std::unordered_map modules; + + size_t count = obs_data_array_count(arr); + for (size_t i = 0; i < count; ++i) { + obs_data_t *obj = obs_data_array_item(arr, i); + VST3ClassInfo ci; + + ci.name = obs_data_get_string(obj, "name"); + ci.id = obs_data_get_string(obj, "id"); + ci.path = obs_data_get_string(obj, "path"); + ci.pluginName = obs_data_get_string(obj, "pluginName"); + ci.discardable = obs_data_get_bool(obj, "discardable"); + + obs_data_release(obj); + + if (ci.path.empty() || !std::filesystem::exists(ci.path)) { + continue; + } + + auto &m = modules[ci.path]; + if (ci.discardable) { + m.discardable = true; + } + + m.classes.push_back(std::move(ci)); + } + + obs_data_array_release(arr); + obs_data_release(root); + + list->updateModulesList(modules); + + for (auto &[modulePath, m] : modules) { + // if a module has the flag kClassesDiscardable, the SDK compels us to do a full load from binary, duh ... + if (m.discardable) { + list->addModuleClasses(modulePath); + } else { + for (auto &ci : m.classes) { + list->pluginList.push_back(ci); + ++list->classCount[modulePath]; + } + } + } + + list->sort(); + + return !list->pluginList.empty(); +} + +bool retrieve_vst3_list() +{ + vst3_scan_done.store(false, std::memory_order_relaxed); + list = new VST3Scanner(); + if (!list->hasVST3()) { + blog(LOG_INFO, "[VST3 Scanner] No VST3 were found"); + return false; + } + + std::thread([] { + using clock = std::chrono::steady_clock; + auto start = clock::now(); + + bool loaded_from_cache = vst3_cache_load(); + if (!loaded_from_cache) { + if (!list->scanForVST3Plugins()) { + blog(LOG_INFO, "[VST3 Scanner] Error when scanning for VST3. Module will be unloaded."); + } + } + + blog(LOG_INFO, "[VST3 Scanner] Available plugins:"); + for (const auto &plugin : list->pluginList) { + blog(LOG_INFO, "[VST3 Scanner] %s", plugin.name.c_str()); + } + + auto end = clock::now(); + auto ms = std::chrono::duration_cast(end - start).count(); + blog(LOG_INFO, "[VST3 Scanner] %s in %lld ms, found %zu plugins", + loaded_from_cache ? "Loaded cache & non-cacheable VST3s" : "Completed scan", + static_cast(ms), list->pluginList.size()); + + vst3_cache_save(); + vst3_scan_done.store(true, std::memory_order_relaxed); + }).detach(); + + return true; +} + +void free_vst3_list() +{ + delete list; +} + +static bool is_valid_hex(const std::string &hex) +{ + if (hex.empty()) { + return false; + } + + if ((hex.size() & 1) != 0) { + return false; + } + + for (char c : hex) { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + return false; + } + } + return true; +} + +std::string toHex(const std::vector &data) +{ + std::ostringstream oss; + for (auto b : data) { + oss << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); + } + + return oss.str(); +} + +std::vector fromHex(const std::string &hex) +{ + if (!is_valid_hex(hex)) { + blog(LOG_INFO, "Corrupted VST3 settings."); + return {}; + } + + std::vector data; + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + data.push_back(static_cast(std::stoi(hex.substr(i, 2), nullptr, 16))); + } + + return data; +} + +Steinberg::Vst::SpeakerArrangement obs_to_vst3_speaker_arrangement(speaker_layout layout) +{ + switch (layout) { + case SPEAKERS_MONO: + return Steinberg::Vst::SpeakerArr::kMono; + case SPEAKERS_STEREO: + return Steinberg::Vst::SpeakerArr::kStereo; + case SPEAKERS_2POINT1: // Steinberg VST3 does not support 2.1 audio, so fallback to 3.0 + return Steinberg::Vst::SpeakerArr::k30Cine; + case SPEAKERS_4POINT0: + return Steinberg::Vst::SpeakerArr::k40Cine; + case SPEAKERS_4POINT1: + return Steinberg::Vst::SpeakerArr::k41Cine; + case SPEAKERS_5POINT1: + return Steinberg::Vst::SpeakerArr::k51; + case SPEAKERS_7POINT1: + return Steinberg::Vst::SpeakerArr::k71Music; + case SPEAKERS_UNKNOWN: + default: + return Steinberg::Vst::SpeakerArr::kEmpty; + } +} + +static inline enum speaker_layout convert_speaker_layout(uint8_t channels) +{ + switch (channels) { + case 0: + return SPEAKERS_UNKNOWN; + case 1: + return SPEAKERS_MONO; + case 2: + return SPEAKERS_STEREO; + case 3: + return SPEAKERS_2POINT1; + case 4: + return SPEAKERS_4POINT0; + case 5: + return SPEAKERS_4POINT1; + case 6: + return SPEAKERS_5POINT1; + case 8: + return SPEAKERS_7POINT1; + default: + return SPEAKERS_UNKNOWN; + } +} + +//--------------------- deque management -------------------------- + +static inline void clear_deque(struct deque *buf) +{ + deque_pop_front(buf, nullptr, buf->size); +} + +static void reset_data(struct vst3_audio_data *vd) +{ + for (size_t i = 0; i < vd->channels; i++) { + clear_deque(&vd->input_buffers[i]); + clear_deque(&vd->output_buffers[i]); + } + + clear_deque(&vd->info_buffer); +} + +static void reset_sidechain_data(struct vst3_audio_data *vd) +{ + std::lock_guard lock(vd->sidechain_mutex); + for (size_t i = 0; i < vd->channels; i++) { + clear_deque(&vd->sc_input_buffers[i]); + } +} + +// -------------------- main functions ------------------- + +static const char *vst3_name(void *unused) +{ + UNUSED_PARAMETER(unused); + return TEXT_PLUGIN; +} + +static void sidechain_capture(void *data, obs_source_t *source, const struct audio_data *audio, bool muted); + +static void vst3_destroy(void *data) +{ + auto *vd = static_cast(data); + vd->bypass.store(true, std::memory_order_relaxed); + vd->sidechain_enabled.store(false, std::memory_order_relaxed); + + if (vd->weak_sidechain) { + obs_source_t *sidechain = obs_weak_source_get_source(vd->weak_sidechain); + if (sidechain) { + obs_source_remove_audio_capture_callback(sidechain, sidechain_capture, vd); + obs_source_release(sidechain); + } + obs_weak_source_release(vd->weak_sidechain); + } + + std::atomic_store(&vd->sc_resampler, std::shared_ptr{}); + vd->sc_last_timestamp = 0; + + for (size_t i = 0; i < vd->channels; i++) { + deque_free(&vd->input_buffers[i]); + deque_free(&vd->output_buffers[i]); + { + std::lock_guard lock(vd->sidechain_mutex); + deque_free(&vd->sc_input_buffers[i]); + } + } + bfree(vd->copy_buffers[0]); + bfree(vd->sc_copy_buffers[0]); + deque_free(&vd->info_buffer); + da_free(vd->output_data); + + auto plugin = std::atomic_load(&vd->plugin); + if (plugin) { + plugin->setProcessing(false); + std::atomic_store(&vd->plugin, std::shared_ptr{}); + } + + delete vd; +} + +static void teardown_sidechain(vst3_audio_data *vd, obs_data *settings) +{ + if (!vd->weak_sidechain || vd->sidechain_name.empty()) { + return; + } + vd->sidechain_enabled.store(false, std::memory_order_relaxed); + + obs_weak_source_t *old_weak = nullptr; + { + std::lock_guard lock(vd->sidechain_update_mutex); + if (vd->weak_sidechain) { + old_weak = vd->weak_sidechain; + vd->weak_sidechain = nullptr; + } + vd->sidechain_name.clear(); + obs_data_set_string(settings, S_SIDECHAIN_SOURCE, nullptr); + } + + if (old_weak) { + obs_source_t *old_source = obs_weak_source_get_source(old_weak); + + if (old_source) { + obs_source_remove_audio_capture_callback(old_source, sidechain_capture, vd); + + obs_source_release(old_source); + } + + obs_weak_source_release(old_weak); + } + + vd->sc_last_timestamp = 0; + std::atomic_store(&vd->sc_resampler, std::shared_ptr{}); +} + +static void destroy_current_VST3Plugin(vst3_audio_data *vd, obs_data *settings) +{ + vd->bypass.store(true, std::memory_order_relaxed); + auto plugin = std::atomic_load(&vd->plugin); + if (!plugin) { + return; + } + + std::atomic_store(&vd->plugin, std::shared_ptr{}); + + plugin->setProcessing(false); + plugin->hideEditor(); + plugin->deactivateComponent(); + vd->noview.store(true, std::memory_order_relaxed); + + if (vd->weak_sidechain) { + teardown_sidechain(vd, settings); + } +} + +static bool create_VST3Plugin(vst3_audio_data *vd) +{ + if (vd->vst3_id.empty() || vd->vst3_path.empty()) { + return true; + } + + const std::string class_id = vd->vst3_id; + const std::string vst3_path = vd->vst3_path; + const int sample_rate = vd->sample_rate; + constexpr int max_block = FRAME_SIZE; + + Steinberg::Vst::SpeakerArrangement arr = obs_to_vst3_speaker_arrangement(vd->layout); + + auto *raw = new VST3Plugin(); + raw->obsVst3Data = vd; + + if (!raw->init(class_id, vst3_path, sample_rate, max_block, arr)) { + infovst3("Failed to initialize VST3 plugin %s", raw->name.c_str()); + vd->last_init_failed = true; + delete raw; + return false; + } else { + infovst3("Plugin %s was successfully initialized.", raw->name.c_str()); + } + + // not all VST3s have a GUI! + if (!raw->createView()) { + infovst3("Failed to create editor view for plugin at: %s", vst3_path.c_str()); + vd->noview.store(true, std::memory_order_relaxed); + } else { + vd->noview.store(false, std::memory_order_relaxed); + infovst3("Plugin %s has a GUI.", raw->name.c_str()); + } + + auto plugin = std::shared_ptr(raw, [](VST3Plugin *p) { + if (p) { + p->deleteLater(); + } + }); + + std::atomic_store(&vd->plugin, plugin); + vd->bypass.store(false, std::memory_order_relaxed); + + return true; +} + +// Main init function; in case of failure, the obs-vst3 filter is bypassed; if the new vst3 is empty, it just deletes +// safely the previous vst3. +static bool init_VST3Plugin(void *data, obs_data *settings) +{ + auto *vd = static_cast(data); + + if (vd->init_in_progress.test_and_set()) { + return false; + } + + struct ClearFlag { + std::atomic_flag &f; + ~ClearFlag() { f.clear(); } + } _guard{vd->init_in_progress}; + + destroy_current_VST3Plugin(vd, settings); + + return create_VST3Plugin(vd); +} + +static void sidechain_swap(vst3_audio_data *vd, obs_data *settings) +{ + if (!vd->has_sidechain.load(std::memory_order_relaxed)) { + return; + } + + vd->sidechain_enabled.store(false, std::memory_order_relaxed); + + std::string sidechain_name(obs_data_get_string(settings, S_SIDECHAIN_SOURCE)); + bool valid_sidechain = sidechain_name != "none" && !sidechain_name.empty(); + obs_weak_source_t *old_weak_sidechain = nullptr; + + { + std::lock_guard lock(vd->sidechain_update_mutex); + if (!valid_sidechain) { + { + if (vd->weak_sidechain) { + old_weak_sidechain = vd->weak_sidechain; + vd->weak_sidechain = nullptr; + } + vd->sidechain_name = ""; + } + } else { + + if (vd->sidechain_name.empty() || vd->sidechain_name != sidechain_name) { + if (vd->weak_sidechain) { + old_weak_sidechain = vd->weak_sidechain; + vd->weak_sidechain = nullptr; + } + vd->sidechain_name = sidechain_name; + vd->sidechain_check_time = os_gettime_ns() - 3000000000; + } + } + } + vd->sidechain_enabled.store(true, std::memory_order_relaxed); + + if (old_weak_sidechain) { + obs_source_t *old_sidechain = obs_weak_source_get_source(old_weak_sidechain); + + if (old_sidechain) { + obs_source_remove_audio_capture_callback(old_sidechain, sidechain_capture, vd); + obs_source_release(old_sidechain); + } + + obs_weak_source_release(old_weak_sidechain); + } +} + +// Our logic differs significantly from a DAW. We indeed allow swapping of VST3s which may or may not have a sc. +// 2 or 3 threads are then involved (UI, audio and possibly video due to the trick of sidechain audio capture +// leveraging video_tick). We've taken great care to implement Ross Bencina's cardinal rule for audio programming. +// http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing but for sidechain there's +// still a mutex that's inherited from the obs-filter compressor... TODO: revisit both filters later to improve that. +static void vst3_update(void *data, obs_data_t *settings) +{ + auto *vd = static_cast(data); + if (!vd) { + return; + } + + std::string vst3_plugin_id(obs_data_get_string(settings, S_PLUGIN)); + + if (vst3_plugin_id.empty()) { + vd->bypass.store(true, std::memory_order_relaxed); + vd->vst3_id.clear(); + vd->vst3_path.clear(); + vd->vst3_name.clear(); + vd->has_sidechain.store(false, std::memory_order_relaxed); + destroy_current_VST3Plugin(vd, settings); + + return; + } + + auto plugin = std::atomic_load(&vd->plugin); + bool initial_load = vd->vst3_id.empty() && !plugin; + bool is_swap = (vd->vst3_id != vst3_plugin_id); + + if (is_swap) { + if (!initial_load) { + destroy_current_VST3Plugin(vd, settings); + } + + if (vd->output_data.array) { + da_free(vd->output_data); + } + vd->vst3_id = vst3_plugin_id; + vd->last_init_failed = false; + + if (!list->getPathById(vst3_plugin_id).empty()) { + vd->vst3_path = list->getPathById(vst3_plugin_id); + vd->vst3_name = list->getNameById(vst3_plugin_id); + } else { + vd->vst3_path = obs_data_get_string(settings, "vst3_path"); + vd->vst3_name = obs_data_get_string(settings, "vst3_name"); + } + + infovst3("filter applied: %s, path: %s", vd->vst3_name.c_str(), vd->vst3_path.c_str()); + + if (init_VST3Plugin(vd, settings)) { + auto plugin2 = std::atomic_load(&vd->plugin); + if (plugin2) { + plugin2->setProcessing(true); + vd->sc_channels = plugin2->sidechainNumChannels; + } + // we support sidechain only for mono or stereo buses (sanity check) + vd->has_sidechain.store(vd->sc_channels == 1 || vd->sc_channels == 2, + std::memory_order_relaxed); + vd->bypass.store(false, std::memory_order_relaxed); + plugin = plugin2; + } else { + infovst3("VST3 failure; plugin deactivated."); + vd->bypass.store(true, std::memory_order_relaxed); + vd->has_sidechain.store(false, std::memory_order_relaxed); + vd->sidechain_enabled.store(false, std::memory_order_relaxed); + } + } + + // Only load the state the first time the filter is loaded + if (plugin && initial_load) { + const char *hexComp = obs_data_get_string(settings, "vst3_state"); + const char *hexCtrl = obs_data_get_string(settings, "vst3_ctrl_state"); + if (hexComp && *hexComp) { + std::vector comp = fromHex(hexComp); + std::vector ctrl; + if (hexCtrl && *hexCtrl) { + ctrl = fromHex(hexCtrl); + } + if (!plugin->loadStates(comp, ctrl)) { + infovst3("VST3 failure; failed to load settings."); + } + } + } + // Sidechain specific code starts here, cf obs-filters/compressor-filter.c for the logic. The sidechain swap is + // done in 2 steps with the swapping proper in the video tick callback after a 3 sec wait. + if (vd->has_sidechain.load(std::memory_order_relaxed)) { + sidechain_swap(vd, settings); + } +} + +static void *vst3_create(obs_data_t *settings, obs_source_t *filter) +{ + auto *vd = new vst3_audio_data(); + + vd->context = filter; + vd->vst3_id = {}; + vd->vst3_name = {}; + vd->vst3_path = {}; + + audio_t *audio = obs_get_audio(); + const struct audio_output_info *aoi = audio_output_get_info(audio); + + constexpr auto frames = static_cast(FRAME_SIZE); + vd->frames = frames; + const size_t channels = audio_output_get_channels(audio); + vd->channels = channels; + vd->sample_rate = audio_output_get_sample_rate(audio); + vd->layout = aoi->speakers; + vd->has_sidechain.store(false, std::memory_order_relaxed); + vd->sidechain_enabled.store(false, std::memory_order_relaxed); + vd->noview.store(true, std::memory_order_relaxed); + + vd->latency = 1000000000LL / (1000 / BUFFER_SIZE_MSEC); + + vd->copy_buffers[0] = static_cast(bmalloc(static_cast(FRAME_SIZE) * channels * sizeof(float))); + vd->sc_copy_buffers[0] = static_cast(bmalloc(FRAME_SIZE * channels * sizeof(float))); + + for (size_t c = 1; c < channels; ++c) { + vd->copy_buffers[c] = vd->copy_buffers[c - 1] + frames; + vd->sc_copy_buffers[c] = vd->sc_copy_buffers[c - 1] + frames; + } + + for (size_t i = 0; i < channels; i++) { + deque_reserve(&vd->input_buffers[i], 8 * frames * sizeof(float)); + deque_reserve(&vd->output_buffers[i], 8 * frames * sizeof(float)); + deque_reserve(&vd->sc_input_buffers[i], 8 * frames * sizeof(float)); + } + + vd->bypass.store(true, std::memory_order_relaxed); + + vst3_update(vd, settings); + return vd; +} + +void vst3_save(void *data, obs_data_t *settings) +{ + auto *vd = static_cast(data); + if (!vd) { + return; + } + + auto plugin = std::atomic_load(&vd->plugin); + if (plugin) { + std::vector comp, ctrl; + if (plugin->saveStates(comp, ctrl)) { + obs_data_set_string(settings, "vst3_state", toHex(comp).c_str()); + if (!ctrl.empty()) { + obs_data_set_string(settings, "vst3_ctrl_state", toHex(ctrl).c_str()); + } else { + obs_data_set_string(settings, "vst3_ctrl_state", ""); + } + } + // We store these because the filter might load before VST3s list has been populated with this info. + obs_data_set_string(settings, "vst3_path", vd->vst3_path.c_str()); + obs_data_set_string(settings, "vst3_name", vd->vst3_name.c_str()); + } +} + +// -------------- audio processing (incl. sc) --------------- +static inline void preprocess_input(struct vst3_audio_data *vd, const std::shared_ptr &plugin) +{ + const int num_channels = static_cast(vd->channels); + const int sc_num_channels = static_cast(vd->sc_channels); + const int frames = static_cast(vd->frames); + const size_t segment_size = vd->frames * sizeof(float); + const bool has_sc = vd->has_sidechain.load(std::memory_order_relaxed); + const bool sc_enabled = vd->sidechain_enabled.load(std::memory_order_relaxed); + + if (has_sc && sc_enabled) { + std::lock_guard lock(vd->sidechain_mutex); + for (int i = 0; i < num_channels; i++) { + if (vd->sc_input_buffers[i].size < segment_size) { + deque_push_back_zero(&vd->sc_input_buffers[i], segment_size); + } + } + } + + for (int i = 0; i < num_channels; i++) { + deque_pop_front(&vd->input_buffers[i], vd->copy_buffers[i], vd->frames * sizeof(float)); + } + + if (has_sc && sc_enabled) { + std::lock_guard lock(vd->sidechain_mutex); + for (int i = 0; i < num_channels; i++) { + deque_pop_front(&vd->sc_input_buffers[i], vd->sc_copy_buffers[i], vd->frames * sizeof(float)); + } + } + + for (int ch = 0; ch < num_channels; ++ch) { + auto *inBuf = vd->copy_buffers[ch]; + float *vstIn = plugin->channelBuffer32(Steinberg::Vst::kInput, ch); + if (inBuf && vstIn) { + memcpy(vstIn, inBuf, frames * sizeof(float)); + } + } + + if (has_sc && sc_enabled) { + const bool needs_resampling = vd->channels != vd->sc_channels && + (vd->sc_channels == 1 || vd->sc_channels == 2); + auto sc_resampler = std::atomic_load(&vd->sc_resampler); + if (needs_resampling && sc_resampler) { + uint8_t *resampled[2] = {nullptr, nullptr}; + uint32_t out_frames; + uint64_t ts_offset; + + if (audio_resampler_resample(sc_resampler.get(), resampled, &out_frames, &ts_offset, + (const uint8_t **)vd->sc_copy_buffers, + static_cast(vd->frames))) { + for (int ch = 0; ch < sc_num_channels; ++ch) { + auto *inBuf = reinterpret_cast(resampled[ch]); + float *vstIn = plugin->auxChannelBuffer32(Steinberg::Vst::kInput, ch); + if (inBuf && vstIn) { + memcpy(vstIn, inBuf, out_frames * sizeof(float)); + } + } + } + } else { + for (int ch = 0; ch < sc_num_channels; ++ch) { + float *inBuf = vd->sc_copy_buffers[ch]; + float *vstIn = plugin->auxChannelBuffer32(Steinberg::Vst::kInput, ch); + if (inBuf && vstIn) { + memcpy(vstIn, inBuf, frames * sizeof(float)); + } + } + } + } +} + +static inline void process(struct vst3_audio_data *vd, const std::shared_ptr &plugin) +{ + const int num_channels = static_cast(vd->channels); + const int frames = static_cast(vd->frames); + + preprocess_input(vd, plugin); + plugin->process(frames); + + for (int ch = 0; ch < num_channels; ++ch) { + auto *outBuf = reinterpret_cast(vd->copy_buffers[ch]); + float *vstOut = plugin->channelBuffer32(Steinberg::Vst::kOutput, ch); + if (outBuf && vstOut) { + memcpy(outBuf, vstOut, frames * sizeof(float)); + } + } + + for (size_t i = 0; i < vd->channels; i++) { + deque_push_back(&vd->output_buffers[i], vd->copy_buffers[i], vd->frames * sizeof(float)); + } +} + +// This re-uses the main logic from obs-filters/noise-suppress.c +static struct obs_audio_data *vst3_filter_audio(void *data, struct obs_audio_data *audio) +{ + auto *vd = static_cast(data); + struct vst3_audio_info info = {}; + size_t segment_size = vd->frames * sizeof(float); + size_t out_size; + auto p = std::atomic_load(&vd->plugin); + bool bypass = vd->bypass.load(std::memory_order_relaxed); + + if (bypass || !p) { + return audio; + } + + if (!p->numEnabledOutputAudioBuses) { + return audio; + } + + // If timestamp has dramatically changed, consider it a new stream of audio data. Clear all deques to prevent + // old audio data from being processed as part of the new data. + if (vd->last_timestamp) { + int64_t diff = llabs(static_cast(vd->last_timestamp) - static_cast(audio->timestamp)); + + if (diff > 1000000000LL) { + reset_data(vd); + } + } + + vd->last_timestamp = audio->timestamp; + + info.frames = audio->frames; + info.timestamp = audio->timestamp; + deque_push_back(&vd->info_buffer, &info, sizeof(info)); + + for (size_t i = 0; i < vd->channels; i++) { + deque_push_back(&vd->input_buffers[i], audio->data[i], audio->frames * sizeof(float)); + } + + while (vd->input_buffers[0].size >= segment_size) { + process(vd, p); + } + + memset(&info, 0, sizeof(info)); + deque_peek_front(&vd->info_buffer, &info, sizeof(info)); + out_size = info.frames * sizeof(float); + + if (vd->output_buffers[0].size < out_size) { + return nullptr; + } + + deque_pop_front(&vd->info_buffer, nullptr, sizeof(info)); + da_resize(vd->output_data, out_size * vd->channels); + + for (size_t i = 0; i < vd->channels; i++) { + vd->output_audio.data[i] = reinterpret_cast(&vd->output_data.array[i * out_size]); + + deque_pop_front(&vd->output_buffers[i], vd->output_audio.data[i], out_size); + } + + vd->running_sample_count += info.frames; + vd->system_time = os_gettime_ns(); + vd->output_audio.frames = info.frames; + vd->output_audio.timestamp = info.timestamp - vd->latency; + return &vd->output_audio; +} + +static void sidechain_capture(void *data, obs_source_t *source, const struct audio_data *audio, bool muted) +{ + UNUSED_PARAMETER(source); + UNUSED_PARAMETER(muted); + auto *vd = static_cast(data); + auto p = std::atomic_load(&vd->plugin); + bool bypass = vd->bypass.load(std::memory_order_relaxed); + bool sc_enabled = vd->sidechain_enabled.load(std::memory_order_relaxed); + + if (bypass || !p) { + return; + } + + if (!sc_enabled) { + return; + } + + if (vd->sc_channels != 1 && vd->sc_channels != 2) { + return; + } + + if (vd->sc_last_timestamp) { + int64_t diff = + llabs(static_cast(vd->sc_last_timestamp) - static_cast(audio->timestamp)); + + if (diff > 1000000000LL) { + reset_sidechain_data(vd); + } + } + + vd->sc_last_timestamp = audio->timestamp; + + { + std::lock_guard lock(vd->sidechain_mutex); + for (size_t i = 0; i < vd->channels; i++) { + deque_push_back(&vd->sc_input_buffers[i], audio->data[i], audio->frames * sizeof(float)); + } + } +} + +// written after obs-filters/compressor-filter.c for the sidechain logic +static void vst3_tick(void *data, float seconds) +{ + auto *vd = static_cast(data); + if (!vd) { + return; + } + + bool has_sc = vd->has_sidechain.load(std::memory_order_relaxed); + + if (!has_sc) { + return; + } + + std::string new_name = {}; + { + std::lock_guard lock(vd->sidechain_update_mutex); + if (!vd->sidechain_name.empty() && !vd->weak_sidechain) { + uint64_t t = os_gettime_ns(); + + if (t - vd->sidechain_check_time > 3000000000) { + new_name = vd->sidechain_name; + vd->sidechain_check_time = t; + } + } + } + + if (!new_name.empty()) { + obs_source_t *sidechain = obs_get_source_by_name(new_name.c_str()); + obs_weak_source_t *weak_sidechain = sidechain ? obs_source_get_weak_source(sidechain) : nullptr; + { + std::lock_guard lock(vd->sidechain_update_mutex); + if (!vd->sidechain_name.empty() && vd->sidechain_name == new_name) { + vd->weak_sidechain = weak_sidechain; + weak_sidechain = nullptr; + } + } + if (sidechain) { + // downmix or upmix if channel count is mismatched + bool needs_resampling = vd->channels != vd->sc_channels; + if (needs_resampling) { + struct resample_info src = {}; + struct resample_info dst = {}; + src.samples_per_sec = vd->sample_rate; + src.format = AUDIO_FORMAT_FLOAT_PLANAR; + src.speakers = convert_speaker_layout(static_cast(vd->channels)); + + dst.samples_per_sec = vd->sample_rate; + dst.format = AUDIO_FORMAT_FLOAT_PLANAR; + dst.speakers = convert_speaker_layout(static_cast(vd->sc_channels)); + + audio_resampler *raw = audio_resampler_create(&dst, &src); + if (!raw) { + std::atomic_store(&vd->sc_resampler, std::shared_ptr{}); + } else { + std::shared_ptr sp(raw, [](audio_resampler *r) { + if (r) { + audio_resampler_destroy(r); + } + }); + std::atomic_store(&vd->sc_resampler, sp); + } + } else { + std::atomic_store(&vd->sc_resampler, std::shared_ptr{}); + } + obs_source_add_audio_capture_callback(sidechain, sidechain_capture, vd); + obs_weak_source_release(weak_sidechain); + obs_source_release(sidechain); + } + } + UNUSED_PARAMETER(seconds); +} + +// ---------------- properties functions --------------------- + +static bool vst3_show_gui_callback(obs_properties_t *props, obs_property_t *p, void *data) +{ + UNUSED_PARAMETER(props); + UNUSED_PARAMETER(p); + auto *vd = static_cast(data); + if (!vd) { + return false; + } + + auto plugin = std::atomic_load(&vd->plugin); + if (!plugin) { + return false; + } + + bool noview = vd->noview.load(std::memory_order_relaxed); + if (noview) { + return false; + } + + if (!plugin->isEditorVisible()) { + plugin->showEditor(); + } else { + plugin->hideEditor(); + } + + return true; +} + +static bool add_sources(void *data, obs_source_t *source) +{ + const auto *info = static_cast(data); + const uint32_t caps = obs_source_get_output_flags(source); + + if (source == info->parent) { + return true; + } + + if ((caps & OBS_SOURCE_AUDIO) == 0) { + return true; + } + + const char *name = obs_source_get_name(source); + obs_property_list_add_string(info->sources, name, name); + return true; +} + +bool on_vst3_changed_cb(void *priv, obs_properties_t *props, obs_property_t *property, obs_data_t *settings) +{ + UNUSED_PARAMETER(property); + UNUSED_PARAMETER(settings); + auto vd = static_cast(priv); + if (!vd) { + return false; + } + + const bool has_sc = vd->has_sidechain.load(std::memory_order_relaxed); + + obs_property_t *gui = obs_properties_get(props, S_EDITOR); + obs_property_set_visible(gui, !vd->noview.load(std::memory_order_relaxed) && !vd->last_init_failed); + + obs_property_t *p = obs_properties_get(props, S_SIDECHAIN_SOURCE); + if (has_sc && !vd->last_init_failed) { + obs_source_t *parent = obs_filter_get_parent(vd->context); + obs_property_list_clear(p); + obs_property_list_add_string(p, obs_module_text("None"), "none"); + struct sidechain_prop_info info = {p, parent}; + obs_enum_sources(add_sources, &info); + obs_property_set_visible(p, true); + } else { + obs_property_set_visible(p, false); + } + + obs_property_t *noview = obs_properties_get(props, S_NOGUI); + obs_property_set_visible(noview, vd->noview.load(std::memory_order_relaxed) && !vd->last_init_failed); + + obs_property_t *err = obs_properties_get(props, S_ERR); + if (err) { + obs_properties_remove_by_name(props, S_ERR); + } + if (vd->last_init_failed) { + obs_property_t *err2 = obs_properties_add_text(props, S_ERR, TEXT_ERR, OBS_TEXT_INFO); + obs_property_text_set_info_type(err2, OBS_TEXT_INFO_ERROR); + } + return true; +} + +static obs_properties_t *vst3_properties(void *data) +{ + auto vd = static_cast(data); + obs_properties_t *props = obs_properties_create(); + obs_property_t *sources; + obs_property_t *vst3list = + obs_properties_add_list(props, S_PLUGIN, TEXT_PLUGIN, OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); + + obs_property_list_add_string(vst3list, obs_module_text("VST3.Select"), ""); + + const bool scanDone = vst3_scan_done.load(std::memory_order_acquire); + + if (scanDone) { + for (const auto &plugin : list->pluginList) { + const bool multi = list->moduleHasMultipleClasses(plugin.path); + std::string display = multi ? plugin.name + " (" + plugin.pluginName + ")" : plugin.name; + std::string value = plugin.id; + obs_property_list_add_string(vst3list, display.c_str(), value.c_str()); + } + } else { + obs_property_set_enabled(vst3list, false); + obs_property_t *scan_err = obs_properties_add_text(props, S_SCAN, TEXT_SCAN, OBS_TEXT_INFO); + obs_property_text_set_info_type(scan_err, OBS_TEXT_INFO_ERROR); + } + + obs_property_t *gui = obs_properties_add_button2(props, S_EDITOR, obs_module_text(TEXT_EDITOR), + vst3_show_gui_callback, nullptr); + obs_property_set_visible(gui, !vd->noview.load(std::memory_order_relaxed) && !vd->last_init_failed); + + sources = obs_properties_add_list(props, S_SIDECHAIN_SOURCE, TEXT_SIDECHAIN_SOURCE, OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + obs_property_set_visible(sources, !vd->last_init_failed); + + obs_property_set_modified_callback2(vst3list, on_vst3_changed_cb, data); + + obs_property_t *noview = obs_properties_add_text(props, S_NOGUI, TEXT_NOGUI, OBS_TEXT_INFO); + obs_property_text_set_info_type(noview, OBS_TEXT_INFO_WARNING); + obs_property_set_visible(noview, vd->noview.load(std::memory_order_relaxed) && !vd->last_init_failed); + + if (vd->last_init_failed) { + obs_property_t *err = obs_properties_add_text(props, S_ERR, TEXT_ERR, OBS_TEXT_INFO); + obs_property_text_set_info_type(err, OBS_TEXT_INFO_ERROR); + } + + return props; +} + +void register_vst3_source() +{ + struct obs_source_info vst3_filter = {}; + vst3_filter.id = "vst3_filter"; + vst3_filter.type = OBS_SOURCE_TYPE_FILTER; + vst3_filter.output_flags = OBS_SOURCE_AUDIO; + vst3_filter.get_name = vst3_name; + vst3_filter.create = vst3_create; + vst3_filter.destroy = vst3_destroy; + vst3_filter.update = vst3_update; + vst3_filter.filter_audio = vst3_filter_audio; + vst3_filter.get_properties = vst3_properties; + vst3_filter.save = vst3_save; + vst3_filter.video_tick = vst3_tick; + obs_register_source(&vst3_filter); +} diff --git a/plugins/obs-vst3/obs-vst3.h b/plugins/obs-vst3/obs-vst3.h new file mode 100644 index 00000000000000..9396de912c18c8 --- /dev/null +++ b/plugins/obs-vst3/obs-vst3.h @@ -0,0 +1,83 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +#define MAX_PREPROC_CHANNELS 8 +#define MAX_SC_CHANNELS 2 +#define BUFFER_SIZE_MSEC 10 +#define FRAME_SIZE 480 + +class VST3HostApp; +class VST3Plugin; + +VST3HostApp *get_host_app() noexcept; + +struct vst3_audio_data { + obs_source_t *context; + + std::shared_ptr plugin = nullptr; + std::string vst3_id; + std::string vst3_path; + std::string vst3_name; + + int sample_rate; + size_t frames; + size_t channels; + speaker_layout layout; + int64_t running_sample_count = 0; + uint64_t system_time = 0; + uint64_t last_timestamp; + uint64_t latency; + + struct deque info_buffer; + struct deque input_buffers[MAX_PREPROC_CHANNELS]; + struct deque output_buffers[MAX_PREPROC_CHANNELS]; + struct deque sc_input_buffers[MAX_PREPROC_CHANNELS]; + + float *copy_buffers[MAX_PREPROC_CHANNELS]; + float *sc_copy_buffers[MAX_PREPROC_CHANNELS]; + + struct obs_audio_data output_audio; + DARRAY(float) output_data; + + std::atomic bypass; + std::atomic sidechain_enabled; + std::atomic noview; + std::atomic_flag init_in_progress = ATOMIC_FLAG_INIT; + + std::atomic has_sidechain; + obs_weak_source_t *weak_sidechain; + std::string sidechain_name; + uint64_t sidechain_check_time; + std::shared_ptr sc_resampler; + size_t sc_channels; + uint64_t sc_last_timestamp; + std::mutex sidechain_update_mutex; + std::mutex sidechain_mutex; + + bool last_init_failed; +}; diff --git a/plugins/obs-vst3/plugin-main.cpp b/plugins/obs-vst3/plugin-main.cpp new file mode 100644 index 00000000000000..b732b033ebbb88 --- /dev/null +++ b/plugins/obs-vst3/plugin-main.cpp @@ -0,0 +1,64 @@ +/****************************************************************************** + Copyright (C) 2025-2026 pkv + This file is part of obs-vst3. + It uses the Steinberg VST3 SDK, which is licensed under MIT license. + See https://github.com/steinbergmedia/vst3sdk for details. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include + +#include + +extern bool load_host(); +extern void unload_host(); +extern std::atomic vst3_scan_done; +extern bool retrieve_vst3_list(); +extern void free_vst3_list(); +extern void register_vst3_source(); + +const char *PLUGIN_VERSION = "1.0.0"; +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("obs-vst3", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "VST3 audio plugin"; +} + +bool obs_module_load(void) +{ + if (!load_host()) { + return false; + } + + if (!retrieve_vst3_list()) { + blog(LOG_INFO, "OBS-VST3: you'll have to install VST3s in order to use this filter."); + } + + register_vst3_source(); + blog(LOG_INFO, "OBS-VST3 filter loaded successfully (version %s)", PLUGIN_VERSION); + return true; +} + +void obs_module_post_load(void) +{ + for (int i = 0; i < 50 && !vst3_scan_done; ++i) { + os_sleep_ms(100); + } +} + +void obs_module_unload() +{ + free_vst3_list(); + unload_host(); +}