From d77bfce2c39883b9021f5a2ea57a371ccb130049 Mon Sep 17 00:00:00 2001 From: RVC-Boss <129054828+RVC-Boss@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:16:30 +0800 Subject: [PATCH] Add RVC Realtime VST2/VST3 source project --- .gitmodules | 9 + RVCRealtimeVST/.gitignore | 17 + RVCRealtimeVST/CMakeLists.txt | 95 +++ RVCRealtimeVST/LICENSE.txt | 13 + RVCRealtimeVST/THIRD_PARTY_NOTICES.md | 40 ++ RVCRealtimeVST/config.h | 41 ++ RVCRealtimeVST/resources/README.txt | 87 +++ .../resources/fonts/Roboto-Regular.ttf | Bin 0 -> 145348 bytes RVCRealtimeVST/resources/main.rc | 37 + RVCRealtimeVST/resources/resource.h | 3 + RVCRealtimeVST/scripts/build.ps1 | 76 ++ .../scripts/prepare-dependencies.ps1 | 76 ++ RVCRealtimeVST/scripts/test-all.ps1 | 91 +++ RVCRealtimeVST/scripts/test-worker.ps1 | 44 ++ RVCRealtimeVST/src/RVCRealtime.cpp | 663 ++++++++++++++++++ RVCRealtimeVST/src/RVCRealtime.h | 96 +++ RVCRealtimeVST/src/RvcParameters.hpp | 68 ++ RVCRealtimeVST/src/SpscRing.hpp | 98 +++ RVCRealtimeVST/src/WorkerClient.cpp | 587 ++++++++++++++++ RVCRealtimeVST/src/WorkerClient.hpp | 87 +++ RVCRealtimeVST/third_party/iPlug2 | 1 + .../licenses/Roboto-Apache-2.0.txt | 201 ++++++ .../third_party/vst2-compat/aeffectx.h | 336 +++++++++ RVCRealtimeVST/third_party/vst2sdk | 1 + RVCRealtimeVST/third_party/vst3sdk | 1 + RVCRealtimeVST/tools/vst2_smoke.cpp | 107 +++ RVCRealtimeVST/tools/worker_smoke.cpp | 94 +++ RVCRealtimeVST/worker/rvc_worker.py | 345 +++++++++ 28 files changed, 3314 insertions(+) create mode 100644 .gitmodules create mode 100644 RVCRealtimeVST/.gitignore create mode 100644 RVCRealtimeVST/CMakeLists.txt create mode 100644 RVCRealtimeVST/LICENSE.txt create mode 100644 RVCRealtimeVST/THIRD_PARTY_NOTICES.md create mode 100644 RVCRealtimeVST/config.h create mode 100644 RVCRealtimeVST/resources/README.txt create mode 100644 RVCRealtimeVST/resources/fonts/Roboto-Regular.ttf create mode 100644 RVCRealtimeVST/resources/main.rc create mode 100644 RVCRealtimeVST/resources/resource.h create mode 100644 RVCRealtimeVST/scripts/build.ps1 create mode 100644 RVCRealtimeVST/scripts/prepare-dependencies.ps1 create mode 100644 RVCRealtimeVST/scripts/test-all.ps1 create mode 100644 RVCRealtimeVST/scripts/test-worker.ps1 create mode 100644 RVCRealtimeVST/src/RVCRealtime.cpp create mode 100644 RVCRealtimeVST/src/RVCRealtime.h create mode 100644 RVCRealtimeVST/src/RvcParameters.hpp create mode 100644 RVCRealtimeVST/src/SpscRing.hpp create mode 100644 RVCRealtimeVST/src/WorkerClient.cpp create mode 100644 RVCRealtimeVST/src/WorkerClient.hpp create mode 160000 RVCRealtimeVST/third_party/iPlug2 create mode 100644 RVCRealtimeVST/third_party/licenses/Roboto-Apache-2.0.txt create mode 100644 RVCRealtimeVST/third_party/vst2-compat/aeffectx.h create mode 160000 RVCRealtimeVST/third_party/vst2sdk create mode 160000 RVCRealtimeVST/third_party/vst3sdk create mode 100644 RVCRealtimeVST/tools/vst2_smoke.cpp create mode 100644 RVCRealtimeVST/tools/worker_smoke.cpp create mode 100644 RVCRealtimeVST/worker/rvc_worker.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a53ddfb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "RVCRealtimeVST/third_party/iPlug2"] + path = RVCRealtimeVST/third_party/iPlug2 + url = https://github.com/iPlug2/iPlug2.git +[submodule "RVCRealtimeVST/third_party/vst3sdk"] + path = RVCRealtimeVST/third_party/vst3sdk + url = https://github.com/steinbergmedia/vst3sdk.git +[submodule "RVCRealtimeVST/third_party/vst2sdk"] + path = RVCRealtimeVST/third_party/vst2sdk + url = https://github.com/Xaymar/vst2sdk.git diff --git a/RVCRealtimeVST/.gitignore b/RVCRealtimeVST/.gitignore new file mode 100644 index 0000000..e3719b0 --- /dev/null +++ b/RVCRealtimeVST/.gitignore @@ -0,0 +1,17 @@ +/build/ +/build-*/ +/dist/ +/logs/ +/tools/vst3-validator-build/ +*.aps +*.db +*.exp +*.ilk +*.lib +*.obj +*.pdb +*.sln +*.suo +*.user +*.vcxproj +*.vcxproj.filters diff --git a/RVCRealtimeVST/CMakeLists.txt b/RVCRealtimeVST/CMakeLists.txt new file mode 100644 index 0000000..58fe301 --- /dev/null +++ b/RVCRealtimeVST/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) + +project(RVCRealtime VERSION 0.1.0 LANGUAGES C CXX RC) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +option(RVC_BUILD_SMOKE_TEST "Build the standalone worker smoke test" ON) + +set(IPLUG_DEPLOY_PLUGINS OFF CACHE BOOL "Keep plugin artifacts inside this project" FORCE) +set(IPLUG2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/third_party/iPlug2" CACHE PATH "iPlug2 root directory") + +if(NOT EXISTS "${IPLUG2_DIR}/Dependencies/IPlug/VST3_SDK/base/source/baseiids.cpp" + OR NOT EXISTS "${IPLUG2_DIR}/Dependencies/IPlug/VST2_SDK/aeffectx.h") + message(FATAL_ERROR + "Prepared VST SDK files are missing. Run scripts/prepare-dependencies.ps1 " + "or use scripts/build.ps1, which runs it automatically.") +endif() + +include(${IPLUG2_DIR}/iPlug2.cmake) +find_package(iPlug2 REQUIRED) + +iplug_add_plugin(${PROJECT_NAME} + SOURCES + src/RVCRealtime.cpp + src/RVCRealtime.h + src/WorkerClient.cpp + src/WorkerClient.hpp + src/SpscRing.hpp + src/RvcParameters.hpp + config.h + resources/resource.h + resources/main.rc + RESOURCES + resources/fonts/Roboto-Regular.ttf + FORMATS + VST2 + VST3 + UI IGRAPHICS + DEFINES + UNICODE + _UNICODE + NOMINMAX) + +if(TARGET RVCRealtime-vst2) + target_include_directories(RVCRealtime-vst2 BEFORE PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vst2-compat" + "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vst2sdk/include") +endif() + +if(MSVC) + foreach(target RVCRealtime-vst2 RVCRealtime-vst3) + if(TARGET ${target}) + target_compile_options(${target} PRIVATE /W4 /permissive- /utf-8) + endif() + endforeach() +endif() + +if(RVC_BUILD_SMOKE_TEST) + add_executable(rvc-worker-smoke + tools/worker_smoke.cpp + src/WorkerClient.cpp + src/WorkerClient.hpp + src/SpscRing.hpp + src/RvcParameters.hpp + config.h) + target_include_directories(rvc-worker-smoke PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/src") + target_compile_definitions(rvc-worker-smoke PRIVATE + UNICODE + _UNICODE + NOMINMAX) + if(MSVC) + target_compile_options(rvc-worker-smoke PRIVATE /W4 /permissive- /utf-8) + endif() + add_custom_command(TARGET rvc-worker-smoke POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/RVCRealtime.resources/worker" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/worker/rvc_worker.py" + "$/RVCRealtime.resources/worker/rvc_worker.py") + + add_executable(rvc-vst2-smoke tools/vst2_smoke.cpp) + target_include_directories(rvc-vst2-smoke PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vst2-compat" + "${CMAKE_CURRENT_SOURCE_DIR}/third_party/vst2sdk/include") + target_compile_definitions(rvc-vst2-smoke PRIVATE + UNICODE + _UNICODE + NOMINMAX) + if(MSVC) + target_compile_options(rvc-vst2-smoke PRIVATE /W4 /permissive- /utf-8) + endif() +endif() diff --git a/RVCRealtimeVST/LICENSE.txt b/RVCRealtimeVST/LICENSE.txt new file mode 100644 index 0000000..16e2eed --- /dev/null +++ b/RVCRealtimeVST/LICENSE.txt @@ -0,0 +1,13 @@ +RVC Realtime Plugin + +Copyright (c) 2026 RVC Realtime contributors + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. +2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +Third-party components retain their respective notices and terms under third_party. diff --git a/RVCRealtimeVST/THIRD_PARTY_NOTICES.md b/RVCRealtimeVST/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..77a6615 --- /dev/null +++ b/RVCRealtimeVST/THIRD_PARTY_NOTICES.md @@ -0,0 +1,40 @@ +# Third-Party Notices + +The RVC Realtime VST source tree uses the following third-party components. +Each dependency remains under its own license. + +## iPlug2 + +- Source: https://github.com/iPlug2/iPlug2 +- Locked commit: `5c2df9dce3f5258acfeff3846a6a9563f382212c` +- License: zlib-style license +- License file: `third_party/iPlug2/LICENSE.txt` + +## Steinberg VST 3 SDK + +- Source: https://github.com/steinbergmedia/vst3sdk +- Locked commit: `58f8da7936800732561402d7936584ca4505de07` +- License: MIT +- License file: `third_party/vst3sdk/LICENSE.txt` + +The required nested SDK repositories are locked by the VST 3 SDK gitlinks. +The build initializes `base`, `cmake`, `pluginterfaces`, and `public.sdk`. + +## Xaymar VST2 SDK + +- Source: https://github.com/Xaymar/vst2sdk +- Locked commit: `339d4f31590bf77c0d0d248e09a380ac6285e069` +- License: BSD-3-Clause +- License file: `third_party/vst2sdk/LICENSE` + +`third_party/vst2-compat/aeffectx.h` supplies the compatibility names used by +iPlug2 and is backed by the BSD-3-Clause ABI declarations above. + +## Roboto + +- Font file: `resources/fonts/Roboto-Regular.ttf` +- License: Apache License 2.0 +- License copy: `third_party/licenses/Roboto-Apache-2.0.txt` + +The font file is byte-identical to the Roboto resource in the locked iPlug2 +source tree. diff --git a/RVCRealtimeVST/config.h b/RVCRealtimeVST/config.h new file mode 100644 index 0000000..2146fff --- /dev/null +++ b/RVCRealtimeVST/config.h @@ -0,0 +1,41 @@ +#pragma once + +#define PLUG_NAME "RVC Realtime" +#define PLUG_MFR "RVC Project" +#define PLUG_VERSION_HEX 0x00010000 +#define PLUG_VERSION_STR "0.1.0" +#define PLUG_UNIQUE_ID 'Rvcr' +#define PLUG_MFR_ID 'Rvcp' +#define PLUG_URL_STR "https://github.com/iPlug2/iPlug2" +#define PLUG_EMAIL_STR "" +#define PLUG_COPYRIGHT_STR "Copyright 2026 RVC Realtime contributors" +#define PLUG_CLASS_NAME RVCRealtime + +#define BUNDLE_NAME "RVCRealtime" +#define BUNDLE_MFR "RVCProject" +#define BUNDLE_DOMAIN "org" +#define SHARED_RESOURCES_SUBPATH "RVCRealtime" + +#define PLUG_CHANNEL_IO "1-1 1-2 2-2" +#define PLUG_LATENCY 12480 +#define PLUG_TYPE 0 +#define PLUG_DOES_MIDI_IN 0 +#define PLUG_DOES_MIDI_OUT 0 +#define PLUG_DOES_MPE 0 +#define PLUG_DOES_STATE_CHUNKS 1 +#define PLUG_HAS_UI 1 +#define PLUG_WIDTH 780 +#define PLUG_HEIGHT 630 +#define PLUG_FPS 30 +#define PLUG_SHARED_RESOURCES 0 +#define PLUG_HOST_RESIZE 1 + +#define VST3_SUBCATEGORY "Fx" +#define ROBOTO_FN "Roboto-Regular.ttf" + +#define RVC_WORKER_RELATIVE_PATH "worker\\rvc_worker.py" +#define RVC_VST2_RESOURCES_DIR "RVCRealtime.resources" +#define RVC_DEFAULT_ROOT "" +#define RVC_DEFAULT_PYTHON "" +#define RVC_DEFAULT_MODEL "" +#define RVC_DEFAULT_INDEX "" diff --git a/RVCRealtimeVST/resources/README.txt b/RVCRealtimeVST/resources/README.txt new file mode 100644 index 0000000..d83d717 --- /dev/null +++ b/RVCRealtimeVST/resources/README.txt @@ -0,0 +1,87 @@ +RVC Realtime - Studio One 安装与使用说明 +======================================== + +适用系统 +-------- +- Windows 10/11 64 位 +- Studio One 64 位 +- 用户已经拥有可正常运行的 RVC 源码和 Python/CUDA 环境整合包 + +压缩包内容 +---------- +VST2\ + RVC Realtime.dll + RVCRealtime.resources\worker\rvc_worker.py + +VST3\ + RVCRealtime.vst3\ + +VST2 安装方法 +------------- +1. 把 VST2 文件夹中的以下两个项目一起复制到 Studio One 已扫描的 VST2 插件目录: + - RVC Realtime.dll + - RVCRealtime.resources 文件夹 +2. DLL 和 RVCRealtime.resources 必须保持同级,不能只复制 DLL。 +3. 常见的自定义 VST2 目录示例:C:\VSTPlugins +4. 在 Studio One 的“选项/位置/VST 插件”中确认该目录已加入扫描列表,然后重新扫描插件。 + +正确示例: +C:\VSTPlugins\RVC Realtime.dll +C:\VSTPlugins\RVCRealtime.resources\worker\rvc_worker.py + +VST3 安装方法 +------------- +1. 把 VST3 文件夹中的整个 RVCRealtime.vst3 文件夹复制到: + C:\Program Files\Common Files\VST3\ +2. 不要只复制 Contents 里面的单个文件。 +3. 复制后在 Studio One 中重新扫描插件。 + +正确示例: +C:\Program Files\Common Files\VST3\RVCRealtime.vst3\Contents\x86_64-win\RVCRealtime.vst3 + +首次使用 +-------- +1. 在 Studio One 的音轨上加载“RVC Realtime”。 +2. 点击 RVC ROOT,选择已有 RVC 源码与环境整合包的根目录。 +3. 插件会自动检测该目录下的 runtime\python.exe。 +4. PYTHON 保持空白时,手动选择整合包中可用的 64 位 python.exe。 +5. 选择 .pth 模型;需要索引检索时再选择 .index 文件。 +6. 点击右下角 ENGINE。状态变成 READY 后开始输出变声结果。 + +RVC 整合包要求 +------------- +所选根目录至少需要包含: +- infer\rtrvc.py +- configs\config.py +- Python 环境及其依赖 +- HuBERT、RMVPE 等原 RVC 实时推理所需文件 +- 用户选择的 .pth 模型 + +配置与日志 +---------- +- 最后一次成功启动的路径配置保存在: + %LOCALAPPDATA%\RVCRealtime\settings.ini +- 运行日志保存在: + %TEMP%\RVCRealtime\logs\ +- Worker 的临时 JSON 配置也保存在上述临时目录,系统清理临时文件时可自动删除。 +- VST2 与 VST3 共用最后一次成功配置。 +- 删除 settings.ini 可以清除已保存的路径。 + +常见问题 +-------- +- Studio One 找不到 VST2:确认已添加 DLL 所在目录并重新扫描。 +- Studio One 找不到 VST3:确认复制的是整个 .vst3 文件夹。 +- VST2 提示 worker resource is missing:RVCRealtime.resources 没有与 DLL 放在同一目录。 +- 启动提示 Python 错误:重新选择 RVC ROOT,并检查 runtime\python.exe 或手动选择 Python。 +- 启动提示模型或源码缺失:检查界面选择路径以及 RVC 整合包内容。 +- 系统提示缺少 MSVC DLL:安装 Microsoft Visual C++ 2015-2022 Redistributable x64。 + +操作说明 +-------- +- 单击并拖动滑块:调整参数。 +- 双击滑块数值:直接输入数字。 +- BLOCK 可选范围为 20~1000 ms。 +- CROSSFADE 可选范围为 10~100 ms。 +- 与原版实时 GUI 一致,实际 SOLA Crossfade 为 CROSSFADE 和 40 ms 中的较小值,与 BLOCK 独立。 +- 例如 BLOCK 20 ms、CROSSFADE 100 ms 时,实际 SOLA Crossfade 为 40 ms。 +- 状态栏的 actual CF 会显示当前实际使用的 Crossfade。 diff --git a/RVCRealtimeVST/resources/fonts/Roboto-Regular.ttf b/RVCRealtimeVST/resources/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3e6e2e76134cd6040a49ccf9961d302f7d139fcd GIT binary patch literal 145348 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 + +ROBOTO_FN TTF "fonts/Roboto-Regular.ttf" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 0,1,0,0 + PRODUCTVERSION 0,1,0,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "FileVersion", PLUG_VERSION_STR + VALUE "ProductVersion", PLUG_VERSION_STR + VALUE "FileDescription", PLUG_NAME + VALUE "InternalName", BUNDLE_NAME + VALUE "ProductName", PLUG_NAME + VALUE "CompanyName", PLUG_MFR + VALUE "LegalCopyright", PLUG_COPYRIGHT_STR + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END diff --git a/RVCRealtimeVST/resources/resource.h b/RVCRealtimeVST/resources/resource.h new file mode 100644 index 0000000..654de80 --- /dev/null +++ b/RVCRealtimeVST/resources/resource.h @@ -0,0 +1,3 @@ +#pragma once + +#define IDR_RVC_FONT 101 diff --git a/RVCRealtimeVST/scripts/build.ps1 b/RVCRealtimeVST/scripts/build.ps1 new file mode 100644 index 0000000..0ac437d --- /dev/null +++ b/RVCRealtimeVST/scripts/build.ps1 @@ -0,0 +1,76 @@ +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +$Build = Join-Path $Root "build" +$Dist = Join-Path $Root "dist" +$PackageName = "RVCRealtime-Win64" +$Package = Join-Path $Dist $PackageName +$Zip = Join-Path $Dist "$PackageName.zip" + +& (Join-Path $PSScriptRoot "prepare-dependencies.ps1") + +cmake -S $Root -B $Build -G "Visual Studio 17 2022" -A x64 +if ($LASTEXITCODE -ne 0) { + throw "CMake configuration failed with exit code $LASTEXITCODE" +} +cmake --build $Build --config Release --target RVCRealtime-vst2 RVCRealtime-vst3 --parallel +if ($LASTEXITCODE -ne 0) { + throw "Plugin build failed with exit code $LASTEXITCODE" +} + +New-Item -ItemType Directory -Force -Path $Dist | Out-Null +$Vst2Source = Join-Path $Build "out\RVCRealtime.dll" +$Vst3Source = Join-Path $Build "out\RVCRealtime.vst3" +$LegacyVst3 = Join-Path $Dist "RVC Realtime.vst3" + +Copy-Item -Force $Vst2Source (Join-Path $Dist "RVC Realtime.dll") +if (Test-Path $LegacyVst3) { + Remove-Item -Recurse -Force $LegacyVst3 +} +if (Test-Path (Join-Path $Dist "RVCRealtime.vst3")) { + Remove-Item -Recurse -Force (Join-Path $Dist "RVCRealtime.vst3") +} +Copy-Item -Recurse -Force $Vst3Source (Join-Path $Dist "RVCRealtime.vst3") +$LooseResources = Join-Path $Dist "RVCRealtime.resources" +if (Test-Path $LooseResources) { + Remove-Item -Recurse -Force $LooseResources +} +New-Item -ItemType Directory -Force -Path (Join-Path $LooseResources "worker") | Out-Null +Copy-Item -Force (Join-Path $Root "worker\rvc_worker.py") (Join-Path $LooseResources "worker\rvc_worker.py") + +$LooseVst3Worker = Join-Path $Dist "RVCRealtime.vst3\Contents\Resources\worker" +New-Item -ItemType Directory -Force -Path $LooseVst3Worker | Out-Null +Copy-Item -Force (Join-Path $Root "worker\rvc_worker.py") (Join-Path $LooseVst3Worker "rvc_worker.py") + +if (Test-Path $Package) { + Remove-Item -Recurse -Force $Package +} +if (Test-Path $Zip) { + Remove-Item -Force $Zip +} +$PackageVst2 = Join-Path $Package "VST2" +$PackageVst3 = Join-Path $Package "VST3" +New-Item -ItemType Directory -Force -Path $PackageVst2, $PackageVst3 | Out-Null +Copy-Item -Force (Join-Path $Dist "RVC Realtime.dll") (Join-Path $PackageVst2 "RVC Realtime.dll") +Copy-Item -Recurse -Force $LooseResources (Join-Path $PackageVst2 "RVCRealtime.resources") +Copy-Item -Recurse -Force (Join-Path $Dist "RVCRealtime.vst3") (Join-Path $PackageVst3 "RVCRealtime.vst3") +$ReadmeSource = Join-Path $Root "resources\README.txt" +$ReadmeDestination = Join-Path $Package "README.txt" +$Utf8WithBom = New-Object System.Text.UTF8Encoding -ArgumentList $true +[System.IO.File]::WriteAllText($ReadmeDestination, [System.IO.File]::ReadAllText($ReadmeSource), $Utf8WithBom) + +$RequiredReleaseFiles = @( + (Join-Path $Package "README.txt"), + (Join-Path $Package "VST2\RVC Realtime.dll"), + (Join-Path $Package "VST2\RVCRealtime.resources\worker\rvc_worker.py"), + (Join-Path $Package "VST3\RVCRealtime.vst3\Contents\x86_64-win\RVCRealtime.vst3"), + (Join-Path $Package "VST3\RVCRealtime.vst3\Contents\Resources\worker\rvc_worker.py") +) +foreach ($RequiredFile in $RequiredReleaseFiles) { + if (-not (Test-Path -LiteralPath $RequiredFile -PathType Leaf)) { + throw "Release package is missing: $RequiredFile" + } +} +Compress-Archive -Path $Package -DestinationPath $Zip -CompressionLevel Optimal + +Write-Host "Built artifacts and release ZIP: $Zip" diff --git a/RVCRealtimeVST/scripts/prepare-dependencies.ps1 b/RVCRealtimeVST/scripts/prepare-dependencies.ps1 new file mode 100644 index 0000000..d693ab0 --- /dev/null +++ b/RVCRealtimeVST/scripts/prepare-dependencies.ps1 @@ -0,0 +1,76 @@ +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +$ThirdParty = Join-Path $Root "third_party" +$IPlug2 = Join-Path $ThirdParty "iPlug2" +$Vst3Source = Join-Path $ThirdParty "vst3sdk" +$Vst2Source = Join-Path $ThirdParty "vst2sdk" +$Vst2Compat = Join-Path $ThirdParty "vst2-compat\aeffectx.h" + +$ExpectedCommits = [ordered]@{ + $IPlug2 = "5c2df9dce3f5258acfeff3846a6a9563f382212c" + $Vst3Source = "58f8da7936800732561402d7936584ca4505de07" + $Vst2Source = "339d4f31590bf77c0d0d248e09a380ac6285e069" +} + +foreach ($Entry in $ExpectedCommits.GetEnumerator()) { + if (-not (Test-Path -LiteralPath (Join-Path $Entry.Key ".git"))) { + throw "Submodule is missing: $($Entry.Key). Run: git submodule update --init --recursive" + } + $ActualCommit = (& git -C $Entry.Key rev-parse HEAD).Trim() + if ($LASTEXITCODE -ne 0 -or $ActualCommit -ne $Entry.Value) { + throw "Unexpected submodule revision at $($Entry.Key): $ActualCommit (expected $($Entry.Value))" + } +} + +$RequiredVst3Modules = @("base", "cmake", "pluginterfaces", "public.sdk") +$RequiredVst3ModuleFiles = @( + "base\source\baseiids.cpp", + "cmake\modules\SMTG_AddVST3Library.cmake", + "pluginterfaces\base\funknown.cpp", + "public.sdk\source\main\dllmain.cpp" +) +$MissingVst3Modules = $RequiredVst3ModuleFiles | Where-Object { + -not (Test-Path -LiteralPath (Join-Path $Vst3Source $_) -PathType Leaf) +} +if ($MissingVst3Modules.Count -gt 0) { + & git -C $Vst3Source submodule update --init @RequiredVst3Modules + if ($LASTEXITCODE -ne 0) { + throw "Failed to initialize the required nested VST3 SDK submodules." + } +} + +$Vst3Destination = Join-Path $IPlug2 "Dependencies\IPlug\VST3_SDK" +New-Item -ItemType Directory -Force -Path $Vst3Destination | Out-Null +foreach ($Directory in @("base", "cmake", "pluginterfaces", "public.sdk")) { + $Source = Join-Path $Vst3Source $Directory + if (-not (Test-Path -LiteralPath $Source -PathType Container)) { + throw "VST3 SDK directory is missing: $Source" + } + Copy-Item -LiteralPath $Source -Destination $Vst3Destination -Recurse -Force +} +foreach ($File in @("CMakeLists.txt", "LICENSE.txt")) { + Copy-Item -LiteralPath (Join-Path $Vst3Source $File) -Destination (Join-Path $Vst3Destination $File) -Force +} + +$Vst2Destination = Join-Path $IPlug2 "Dependencies\IPlug\VST2_SDK" +New-Item -ItemType Directory -Force -Path $Vst2Destination | Out-Null +Get-ChildItem -LiteralPath (Join-Path $Vst2Source "include") -File | ForEach-Object { + Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $Vst2Destination $_.Name) -Force +} +Copy-Item -LiteralPath $Vst2Compat -Destination (Join-Path $Vst2Destination "aeffectx.h") -Force +Copy-Item -LiteralPath (Join-Path $Vst2Source "LICENSE") -Destination (Join-Path $Vst2Destination "LICENSE.BSD-3-Clause.txt") -Force + +$RequiredFiles = @( + (Join-Path $Vst3Destination "base\source\baseiids.cpp"), + (Join-Path $Vst3Destination "public.sdk\source\main\dllmain.cpp"), + (Join-Path $Vst2Destination "aeffectx.h"), + (Join-Path $Vst2Destination "vst.h") +) +foreach ($RequiredFile in $RequiredFiles) { + if (-not (Test-Path -LiteralPath $RequiredFile -PathType Leaf)) { + throw "Prepared dependency file is missing: $RequiredFile" + } +} + +Write-Host "Prepared locked iPlug2, VST3, and VST2 dependencies." diff --git a/RVCRealtimeVST/scripts/test-all.ps1 b/RVCRealtimeVST/scripts/test-all.ps1 new file mode 100644 index 0000000..d8a56c6 --- /dev/null +++ b/RVCRealtimeVST/scripts/test-all.ps1 @@ -0,0 +1,91 @@ +param( + [switch]$SkipWorker, + [string]$RvcRoot = "", + [string]$Python = "", + [string]$Model = "", + [string]$Index = "" +) + +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +$Build = Join-Path $Root "build" +$Dist = Join-Path $Root "dist" +$ValidatorBuild = Join-Path $Build "v3vs" +$Validator = Join-Path $ValidatorBuild "bin\Release\validator.exe" + +& (Join-Path $PSScriptRoot "build.ps1") + +cmake -S $Root -B $Build -G "Visual Studio 17 2022" -A x64 -DRVC_BUILD_SMOKE_TEST=ON +if ($LASTEXITCODE -ne 0) { + throw "CMake configuration failed with exit code $LASTEXITCODE" +} +cmake --build $Build --config Release --target rvc-vst2-smoke rvc-worker-smoke --parallel +if ($LASTEXITCODE -ne 0) { + throw "Smoke-test build failed with exit code $LASTEXITCODE" +} + +& (Join-Path $Build "Release\rvc-vst2-smoke.exe") (Join-Path $Dist "RVC Realtime.dll") +if ($LASTEXITCODE -ne 0) { + throw "VST2 smoke test failed with exit code $LASTEXITCODE" +} + +if (-not (Test-Path $Validator)) { + $Sdk = Join-Path $Root "third_party\iPlug2\Dependencies\IPlug\VST3_SDK" + # Several validator source names exceed legacy MSBuild path limits when the + # repository is cloned deeply. A stable TEMP junction keeps compiler paths short. + $ValidatorSdk = Join-Path ([System.IO.Path]::GetTempPath()) "RVCRealtimeVST-vst3sdk-58f8da7" + if (-not (Test-Path -LiteralPath $ValidatorSdk)) { + New-Item -ItemType Junction -Path $ValidatorSdk -Target $Sdk | Out-Null + } + if (-not (Test-Path -LiteralPath (Join-Path $ValidatorSdk "CMakeLists.txt") -PathType Leaf)) { + throw "Short VST3 SDK path is invalid: $ValidatorSdk" + } + cmake -S $ValidatorSdk -B $ValidatorBuild -G "Visual Studio 17 2022" -A x64 ` + -DSMTG_ENABLE_VST3_PLUGIN_EXAMPLES=OFF ` + -DSMTG_ENABLE_VST3_HOSTING_EXAMPLES=ON ` + -DSMTG_ENABLE_VSTGUI_SUPPORT=OFF + if ($LASTEXITCODE -ne 0) { + throw "VST3 validator configuration failed with exit code $LASTEXITCODE" + } + cmake --build $ValidatorBuild --config Release --target validator --parallel + if ($LASTEXITCODE -ne 0) { + throw "VST3 validator build failed with exit code $LASTEXITCODE" + } +} + +& $Validator (Join-Path $Dist "RVCRealtime.vst3") +if ($LASTEXITCODE -ne 0) { + throw "VST3 validator failed with exit code $LASTEXITCODE" +} + +if (-not $SkipWorker) { + if ([string]::IsNullOrWhiteSpace($RvcRoot) -or [string]::IsNullOrWhiteSpace($Model)) { + throw "Specify -RvcRoot and -Model for the CUDA worker test, or use -SkipWorker." + } + if ([string]::IsNullOrWhiteSpace($Python)) { + $Python = Join-Path $RvcRoot "runtime\python.exe" + } + foreach ($RequiredPath in @($RvcRoot, $Python, $Model)) { + if (-not (Test-Path -LiteralPath $RequiredPath)) { + throw "Required worker test path does not exist: $RequiredPath" + } + } + if (-not [string]::IsNullOrWhiteSpace($Index) -and -not (Test-Path -LiteralPath $Index -PathType Leaf)) { + throw "Index path does not exist: $Index" + } + $WorkerArgs = @($RvcRoot, $Python, $Model) + if (-not [string]::IsNullOrWhiteSpace($Index)) { + $WorkerArgs += $Index + } + & (Join-Path $Build "Release\rvc-worker-smoke.exe") @WorkerArgs + if ($LASTEXITCODE -ne 0) { + throw "Worker smoke test failed with exit code $LASTEXITCODE" + } +} + +if ($SkipWorker) { + Write-Host "VST2 and VST3 format tests passed; CUDA worker test skipped." +} else { + Write-Host "All VST2, VST3, and CUDA worker tests passed." +} diff --git a/RVCRealtimeVST/scripts/test-worker.ps1 b/RVCRealtimeVST/scripts/test-worker.ps1 new file mode 100644 index 0000000..280bf79 --- /dev/null +++ b/RVCRealtimeVST/scripts/test-worker.ps1 @@ -0,0 +1,44 @@ +param( + [Parameter(Mandatory = $true)] + [string]$RvcRoot, + [string]$Python = "", + [Parameter(Mandatory = $true)] + [string]$Model, + [string]$Index = "" +) + +$ErrorActionPreference = "Stop" + +$Root = Split-Path -Parent $PSScriptRoot +$Build = Join-Path $Root "build" + +& (Join-Path $PSScriptRoot "prepare-dependencies.ps1") + +if ([string]::IsNullOrWhiteSpace($Python)) { + $Python = Join-Path $RvcRoot "runtime\python.exe" +} +foreach ($RequiredPath in @($RvcRoot, $Python, $Model)) { + if (-not (Test-Path -LiteralPath $RequiredPath)) { + throw "Required worker test path does not exist: $RequiredPath" + } +} +if (-not [string]::IsNullOrWhiteSpace($Index) -and -not (Test-Path -LiteralPath $Index -PathType Leaf)) { + throw "Index path does not exist: $Index" +} + +cmake -S $Root -B $Build -G "Visual Studio 17 2022" -A x64 -DRVC_BUILD_SMOKE_TEST=ON +if ($LASTEXITCODE -ne 0) { + throw "CMake configuration failed with exit code $LASTEXITCODE" +} +cmake --build $Build --config Release --target rvc-worker-smoke --parallel +if ($LASTEXITCODE -ne 0) { + throw "Worker smoke-test build failed with exit code $LASTEXITCODE" +} +$WorkerArgs = @($RvcRoot, $Python, $Model) +if (-not [string]::IsNullOrWhiteSpace($Index)) { + $WorkerArgs += $Index +} +& (Join-Path $Build "Release\rvc-worker-smoke.exe") @WorkerArgs +if ($LASTEXITCODE -ne 0) { + throw "Worker smoke test failed with exit code $LASTEXITCODE" +} diff --git a/RVCRealtimeVST/src/RVCRealtime.cpp b/RVCRealtimeVST/src/RVCRealtime.cpp new file mode 100644 index 0000000..8e0c1ef --- /dev/null +++ b/RVCRealtimeVST/src/RVCRealtime.cpp @@ -0,0 +1,663 @@ +#include "RVCRealtime.h" +#include "IPlug_include_in_plug_src.h" +#include "IPlugPaths.h" +#include "IControls.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace { + +const IColor kBackground = IColor(255, 18, 20, 23); +const IColor kPanel = IColor(255, 33, 36, 39); +const IColor kHeaderColor = IColor(255, 235, 234, 228); +const IColor kText = IColor(255, 233, 232, 226); +const IColor kMuted = IColor(255, 152, 155, 155); +const IColor kAccent = IColor(255, 41, 151, 126); +const IColor kAmber = IColor(255, 230, 160, 48); + +enum class PathRow { RvcRoot, Python, Model, Index }; + +std::wstring SettingsFilePath(const bool createDirectory) +{ + WDL_String directory; + iplug::INIPath(directory, BUNDLE_NAME); + const UTF8AsUTF16 directoryWide(directory.Get()); + if (createDirectory) + CreateDirectoryW(directoryWide.Get(), nullptr); + std::wstring result(directoryWide.Get()); + result += L"\\settings.ini"; + return result; +} + +std::string ReadSetting(const wchar_t* key) +{ + const std::wstring settingsPath = SettingsFilePath(false); + std::vector value(32768, L'\0'); + GetPrivateProfileStringW(L"Paths", key, L"", value.data(), static_cast(value.size()), settingsPath.c_str()); + return UTF16AsUTF8(value.data()).Get(); +} + +void WriteSetting(const std::wstring& settingsPath, const wchar_t* key, const std::string& value) +{ + WritePrivateProfileStringW(L"Paths", key, UTF8AsUTF16(value.c_str()).Get(), settingsPath.c_str()); +} + +bool PathIsFile(const std::string& path) +{ + if (path.empty()) + return false; + const DWORD attributes = GetFileAttributesW(UTF8AsUTF16(path.c_str()).Get()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +bool PathIsDirectory(const std::string& path) +{ + if (path.empty()) + return false; + const DWORD attributes = GetFileAttributesW(UTF8AsUTF16(path.c_str()).Get()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +} + +std::string TrimTrailingSeparators(std::string path) +{ + while (path.size() > 3 && (path.back() == '\\' || path.back() == '/')) + path.pop_back(); + return path; +} + +std::string JoinPath(const std::string& root, const char* relative) +{ + if (root.empty()) + return {}; + std::string result = TrimTrailingSeparators(root); + result += "\\"; + result += relative; + return result; +} + +std::string ParentDirectory(const std::string& filePath) +{ + const std::size_t separator = filePath.find_last_of("\\/"); + return separator == std::string::npos ? std::string() : filePath.substr(0, separator); +} + +bool Is64BitExecutable(const std::string& path) +{ + DWORD binaryType = 0; + return GetBinaryTypeW(UTF8AsUTF16(path.c_str()).Get(), &binaryType) != FALSE + && binaryType == SCS_64BIT_BINARY; +} + +IVStyle MakeStyle() +{ + return DEFAULT_STYLE + .WithColor(kBG, kPanel) + .WithColor(kFG, kAccent) + .WithColor(kPR, kText) + .WithColor(kFR, IColor(255, 64, 68, 72)) + .WithLabelText(IText(13.f, kMuted, "Roboto-Regular", EAlign::Center)) + .WithValueText(IText(14.f, kText, "Roboto-Regular", EAlign::Center)) + .WithRoundness(0.15f) + .WithWidgetFrac(0.55f) + .WithDrawShadows(false) + .WithDrawFrame(false); +} + +class RVCSliderControl final : public IVSliderControl +{ +public: + using IVSliderControl::IVSliderControl; + + void OnMouseDown(float x, float y, const IMouseMod& mod) override + { + ISliderControlBase::OnMouseDown(x, y, mod); + } + + void OnMouseDblClick(float x, float y, const IMouseMod&) override + { + if (mStyle.showValue && mValueBounds.Contains(x, y)) + PromptUserInput(mValueBounds); + } +}; + +const char* StatusName(const int status) +{ + switch (status) { + case rvc::kStatusStarting: return "STARTING"; + case rvc::kStatusLoading: return "LOADING MODEL"; + case rvc::kStatusReady: return "READY"; + case rvc::kStatusError: return "ERROR"; + default: return "ENGINE OFF"; + } +} + +} // namespace + +RVCRealtime::RVCRealtime(const InstanceInfo& info) +: Plugin(info, MakeConfig(kNumParams, kNumPresets)) +{ + GetParam(kEngine)->InitBool("Engine", false); + GetParam(kPitch)->InitInt("Pitch", 12, -24, 24, "st"); + GetParam(kFormant)->InitDouble("Formant", 0.0, -12.0, 12.0, 0.01, "st"); + GetParam(kIndexRate)->InitDouble("Index", 0.0, 0.0, 1.0, 0.001); + GetParam(kRmsMix)->InitDouble("RMS Mix", 0.5, 0.0, 1.0, 0.001); + GetParam(kThreshold)->InitDouble("Gate", -60.0, -60.0, 0.0, 0.1, "dB"); + GetParam(kBlockMs)->InitInt("Block", 130, 20, 1000, "ms"); + GetParam(kCrossfadeMs)->InitInt("Crossfade", 80, 10, 100, "ms"); + GetParam(kExtraMs)->InitInt("Context", 2000, 500, 3000, "ms"); + GetParam(kF0Method)->InitEnum("F0 Method", 0, {"RMVPE", "FCPE", "PM"}); + GetParam(kDryWet)->InitDouble("Mix", 100.0, 0.0, 100.0, 0.1, "%"); + GetParam(kOutputGain)->InitDouble("Output", 0.0, -18.0, 12.0, 0.1, "dB"); + + LoadUserConfiguration(); + if (mPythonPath.GetLength() == 0 && mRvcRoot.GetLength() > 0) { + const std::string detected = JoinPath(mRvcRoot.Get(), "runtime\\python.exe"); + if (PathIsFile(detected)) + mPythonPath.Set(detected.c_str()); + } + const std::string modelDirectory = ParentDirectory(mModelPath.Get()); + const std::string indexDirectory = ParentDirectory(mIndexPath.Get()); + mModelBrowseDirectory.Set(modelDirectory.empty() ? mRvcRoot.Get() : modelDirectory.c_str()); + mIndexBrowseDirectory.Set(indexDirectory.empty() ? mRvcRoot.Get() : indexDirectory.c_str()); + + mWorker.setPath(rvc::kStateModelPath, mModelPath.Get()); + mWorker.setPath(rvc::kStateIndexPath, mIndexPath.Get()); + mWorker.setPath(rvc::kStateRvcRoot, mRvcRoot.Get()); + mWorker.setPath(rvc::kStatePythonPath, mPythonPath.Get()); + SyncParametersToWorker(); + +#if IPLUG_EDITOR + mMakeGraphicsFunc = [&]() { + return MakeGraphics(*this, PLUG_WIDTH, PLUG_HEIGHT, PLUG_FPS, + GetScaleForScreen(PLUG_WIDTH, PLUG_HEIGHT)); + }; + + mLayoutFunc = [&](IGraphics* graphics) { + graphics->EnableMouseOver(true); + graphics->AttachCornerResizer(EUIResizerMode::Scale, false); // drag corner to scale whole UI + graphics->AttachPanelBackground(kBackground); + graphics->LoadFont("Roboto-Regular", ROBOTO_FN); + const IVStyle style = MakeStyle(); + const IRECT bounds = graphics->GetBounds(); + + // Header + graphics->AttachControl(new IPanelControl(bounds.GetFromTop(86.f), kHeaderColor)); + graphics->AttachControl(new ITextControl(IRECT(28, 10, 420, 52), "RVC REALTIME", + IText(28.f, IColor(255, 23, 25, 27), "Roboto-Regular", EAlign::Near))); + graphics->AttachControl(new ITextControl(IRECT(30, 50, 420, 76), "VOICE CONVERSION / CUDA BRIDGE", + IText(12.f, IColor(255, 85, 89, 90), "Roboto-Regular", EAlign::Near))); + graphics->AttachControl(new ITextControl(IRECT(560, 16, 750, 42), "ENGINE OFF", + IText(14.f, IColor(255, 80, 84, 86), "Roboto-Regular", EAlign::Far)), kCtrlStatus); + graphics->AttachControl(new ITextControl(IRECT(560, 42, 750, 66), "0 ms / 0 drop", + IText(12.f, IColor(255, 100, 104, 105), "Roboto-Regular", EAlign::Far)), kCtrlPerformance); + + // Runtime and model paths + auto attachFileRow = [&](const float y, const char* label, const int textTag, const PathRow pathRow) { + const float rowHeight = 36.f; + graphics->AttachControl(new ITextControl(IRECT(30, y, 92, y + rowHeight), label, + IText(13.f, kMuted, "Roboto-Regular", EAlign::Near))); + graphics->AttachControl(new IPanelControl(IRECT(96, y, 692, y + rowHeight), kPanel)); + const char* initial = ""; + switch (pathRow) { + case PathRow::RvcRoot: initial = mRvcRoot.Get(); break; + case PathRow::Python: initial = mPythonPath.Get(); break; + case PathRow::Model: initial = mModelPath.get_filepart(); break; + case PathRow::Index: initial = mIndexPath.get_filepart(); break; + } + graphics->AttachControl(new ITextControl(IRECT(110, y, 680, y + rowHeight), initial, + IText(12.f, kText, "Roboto-Regular", EAlign::Near)), textTag); + auto action = [this, graphics, pathRow](IControl*) { + switch (pathRow) { + case PathRow::RvcRoot: ChooseRvcRoot(graphics); break; + case PathRow::Python: ChoosePython(graphics); break; + case PathRow::Model: ChooseModel(graphics); break; + case PathRow::Index: ChooseIndex(graphics); break; + } + }; + graphics->AttachControl(new IVButtonControl(IRECT(700, y, 750, y + rowHeight), action, "...", + style.WithLabelText(IText(18.f, kText, "Roboto-Regular", EAlign::Center)), true)); + }; + attachFileRow(100.f, "RVC ROOT", kCtrlRvcRoot, PathRow::RvcRoot); + attachFileRow(142.f, "PYTHON", kCtrlPythonPath, PathRow::Python); + attachFileRow(184.f, "MODEL", kCtrlModelName, PathRow::Model); + attachFileRow(226.f, "INDEX", kCtrlIndexName, PathRow::Index); + graphics->AttachControl(new ITextControl(IRECT(96, 266, 750, 290), "Select RVC root and Python runtime", + IText(12.f, kAmber, "Roboto-Regular", EAlign::Near)), kCtrlStatusDetail); + + // 3x3 slider grid + struct SliderLayout { int param; const char* label; }; + const SliderLayout sliders[] = { + {kPitch, "PITCH"}, {kFormant, "FORMANT"}, {kIndexRate, "INDEX"}, + {kRmsMix, "RMS MIX"}, {kThreshold, "GATE"}, {kDryWet, "MIX"}, + {kBlockMs, "BLOCK"}, {kCrossfadeMs, "CROSSFADE"}, {kExtraMs, "CONTEXT"} + }; + const float gridLeft = 30.f, gridTop = 300.f; + const float cellWidth = 226.f, cellHeight = 62.f; + const float gapX = 21.f, gapY = 10.f; + for (int i = 0; i < 9; ++i) { + const int column = i % 3; + const int row = i / 3; + const float x = gridLeft + column * (cellWidth + gapX); + const float y = gridTop + row * (cellHeight + gapY); + const IRECT cell(x, y, x + cellWidth, y + cellHeight); + graphics->AttachControl(new IPanelControl(cell, kPanel)); + graphics->AttachControl(new RVCSliderControl(cell.GetPadded(-8.f), sliders[i].param, + sliders[i].label, style, true, EDirection::Horizontal)); + } + + // Bottom row: F0 method, output gain, engine toggle — one shared baseline + const float bottomY = 566.f, bottomHeight = 44.f; + graphics->AttachControl(new ITextControl(IRECT(30, bottomY, 100, bottomY + bottomHeight), "F0 METHOD", + IText(12.f, kMuted, "Roboto-Regular", EAlign::Near))); + graphics->AttachControl(new IVMenuButtonControl(IRECT(104, bottomY, 236, bottomY + bottomHeight), kF0Method, + "", style)); + graphics->AttachControl(new IPanelControl(IRECT(258, bottomY, 560, bottomY + bottomHeight), kPanel)); + graphics->AttachControl(new RVCSliderControl(IRECT(266, bottomY + 2, 552, bottomY + bottomHeight - 2), kOutputGain, + "OUTPUT", style, true, EDirection::Horizontal)); + graphics->AttachControl(new IVToggleControl(IRECT(600, bottomY, 750, bottomY + bottomHeight), kEngine, + "ENGINE", style)); + }; +#endif +} + +#if IPLUG_DSP +void RVCRealtime::OnReset() +{ + const double sampleRate = GetSampleRate() > 0.0 ? GetSampleRate() : 48000.0; + const int blockSize = std::max(1, GetBlockSize()); + mWorker.setSampleRate(sampleRate); + ResizeBuffers(blockSize, sampleRate); + SyncParametersToWorker(); + const int latency = CalculateLatencyFrames(GetParam(kBlockMs)->Value()); + mTargetDelayFrames.store(latency, std::memory_order_relaxed); + SetLatency(latency); +} + +void RVCRealtime::OnParamChange(const int paramIdx) +{ + if (paramIdx < 0 || paramIdx >= kNumParams) + return; + double value = GetParam(paramIdx)->Value(); + if (paramIdx == kDryWet) + value /= 100.0; + mWorker.setParameter(static_cast(paramIdx), static_cast(value)); + if (paramIdx == kEngine) { + if (value >= 0.5) { + std::string error; + if (!ValidateConfiguration(error)) { + { + std::lock_guard lock(mStateMutex); + mValidationMessage.Set(error.c_str()); + } + GetParam(kEngine)->Set(0.0); + SendParameterValueFromAPI(kEngine, 0.0, false); + mWorker.setEnabled(false); + return; + } + { + std::lock_guard lock(mStateMutex); + mValidationMessage.Set(""); + } + SaveUserConfiguration(); + mWorker.setEnabled(true); + } else { + mWorker.setEnabled(false); + } + } + if (paramIdx == kBlockMs) { + const int latency = CalculateLatencyFrames(value); + mTargetDelayFrames.store(latency, std::memory_order_relaxed); + SetLatency(latency); + } +} + +void RVCRealtime::ProcessBlock(sample** inputs, sample** outputs, const int nFrames) +{ + const int nIn = NInChansConnected(); + const int nOut = NOutChansConnected(); + if (nFrames <= 0 || nOut <= 0) + return; + + if (nIn <= 0) { + for (int channel = 0; channel < nOut; ++channel) + std::memset(outputs[channel], 0, static_cast(nFrames) * sizeof(sample)); + return; + } + + // Mono-safe passthrough: never leave output buffers unwritten. + auto passthrough = [&]() { + for (int channel = 0; channel < nOut; ++channel) { + const sample* source = inputs[std::min(channel, nIn - 1)]; + if (outputs[channel] != source) + std::memcpy(outputs[channel], source, static_cast(nFrames) * sizeof(sample)); + } + }; + + if (nFrames > static_cast(mMonoInput.size())) { + passthrough(); + return; + } + + const bool requested = GetParam(kEngine)->Bool(); + if (!requested || !mWorker.isReady()) { + mActiveBlend = std::max(0.0f, mActiveBlend - static_cast(nFrames) / 1024.0f); + passthrough(); + return; + } + + const sample* inL = inputs[0]; + const sample* inR = inputs[nIn > 1 ? 1 : 0]; + sample* outL = outputs[0]; + sample* outR = nOut > 1 ? outputs[1] : nullptr; + + for (int frame = 0; frame < nFrames; ++frame) + mMonoInput[static_cast(frame)] = static_cast(0.5 * (inL[frame] + inR[frame])); + mWorker.pushInput(mMonoInput.data(), static_cast(nFrames)); + const size_t wetRead = mWorker.popOutput(mWetOutput.data(), static_cast(nFrames)); + std::fill(mWetOutput.begin() + static_cast(wetRead), + mWetOutput.begin() + nFrames, 0.0f); + + const float mix = static_cast(GetParam(kDryWet)->Value() / 100.0); + const float gain = static_cast(std::pow(10.0, GetParam(kOutputGain)->Value() / 20.0)); + const int maxDelay = static_cast(mDryDelay.size() / 2); + const int delay = std::clamp(mTargetDelayFrames.load(std::memory_order_relaxed), 0, maxDelay - 1); + const int readPosition = (mDryWritePosition + maxDelay - delay) % maxDelay; + mActiveBlend = std::min(1.0f, mActiveBlend + static_cast(nFrames) / 2048.0f); + + for (int frame = 0; frame < nFrames; ++frame) { + const int write = (mDryWritePosition + frame) % maxDelay; + const int read = (readPosition + frame) % maxDelay; + mDryDelay[static_cast(write) * 2] = static_cast(inL[frame]); + mDryDelay[static_cast(write) * 2 + 1] = static_cast(inR[frame]); + const float wet = mWetOutput[static_cast(frame)] * gain; + const float processedL = mDryDelay[static_cast(read) * 2] * (1.0f - mix) + wet * mix; + const float processedR = mDryDelay[static_cast(read) * 2 + 1] * (1.0f - mix) + wet * mix; + outL[frame] = static_cast(inL[frame] * (1.0f - mActiveBlend) + processedL * mActiveBlend); + if (outR) + outR[frame] = static_cast(inR[frame] * (1.0f - mActiveBlend) + processedR * mActiveBlend); + } + mDryWritePosition = (mDryWritePosition + nFrames) % maxDelay; + + // Copy to any extra connected outputs beyond stereo. + for (int channel = 2; channel < nOut; ++channel) + std::memcpy(outputs[channel], outputs[channel & 1], static_cast(nFrames) * sizeof(sample)); +} +#endif + +void RVCRealtime::OnIdle() +{ +#if IPLUG_EDITOR + if (GetUI() == nullptr) + return; + std::string validationMessage; + { + std::lock_guard lock(mStateMutex); + validationMessage = mValidationMessage.Get(); + } + if (auto* status = GetUI()->GetControlWithTag(kCtrlStatus)) + status->As()->SetStr(validationMessage.empty() ? StatusName(mWorker.status()) : "CONFIG ERROR"); + if (auto* detail = GetUI()->GetControlWithTag(kCtrlStatusDetail)) + detail->As()->SetStr(validationMessage.empty() ? mWorker.statusText().c_str() : validationMessage.c_str()); + if (auto* performance = GetUI()->GetControlWithTag(kCtrlPerformance)) + performance->As()->SetStrFmt(80, "%.0f ms / %.0f drop", mWorker.inferMs(), mWorker.droppedBlocks()); +#endif +} + +void RVCRealtime::OnUIOpen() +{ + Plugin::OnUIOpen(); // pushes current param values to the UI controls + UpdateFileLabels(); +} + +bool RVCRealtime::SerializeState(IByteChunk& chunk) const +{ + std::lock_guard lock(mStateMutex); + chunk.PutStr(mModelPath.Get()); + chunk.PutStr(mIndexPath.Get()); + chunk.PutStr(mRvcRoot.Get()); + chunk.PutStr(mPythonPath.Get()); + return SerializeParams(chunk); +} + +int RVCRealtime::UnserializeState(const IByteChunk& chunk, int startPos) +{ + WDL_String model, index, root, python; + startPos = chunk.GetStr(model, startPos); + startPos = chunk.GetStr(index, startPos); + startPos = chunk.GetStr(root, startPos); + startPos = chunk.GetStr(python, startPos); + if (startPos < 0) + return startPos; + { + std::lock_guard lock(mStateMutex); + mModelPath.Set(model.Get()); + mIndexPath.Set(index.Get()); + mRvcRoot.Set(root.Get()); + mPythonPath.Set(python.Get()); + } + mWorker.setPath(rvc::kStateModelPath, model.Get()); + mWorker.setPath(rvc::kStateIndexPath, index.Get()); + mWorker.setPath(rvc::kStateRvcRoot, root.Get()); + mWorker.setPath(rvc::kStatePythonPath, python.Get()); + startPos = UnserializeParams(chunk, startPos); + SyncParametersToWorker(); + UpdateFileLabels(); + return startPos; +} + +void RVCRealtime::SyncParametersToWorker() +{ + for (int i = 0; i < kNumParams; ++i) + OnParamChange(i); +} + +int RVCRealtime::CalculateLatencyFrames(const double blockMs) const +{ + const double sampleRate = GetSampleRate() > 0.0 ? GetSampleRate() : 48000.0; + const double zc = std::max(1.0, std::floor(sampleRate / 100.0)); + const int block = static_cast(std::round(blockMs / 1000.0 * sampleRate / zc) * zc); + return block * 2; +} + +void RVCRealtime::ResizeBuffers(const int blockSize, const double sampleRate) +{ + const size_t scratch = static_cast(std::max(blockSize, 131072)); + mMonoInput.assign(scratch, 0.0f); + mWetOutput.assign(scratch, 0.0f); + mDryDelay.assign(static_cast(std::max(1.0, sampleRate * 4.0)) * 2, 0.0f); + mDryWritePosition = 0; + mActiveBlend = 0.0f; +} + +void RVCRealtime::ChooseRvcRoot(IGraphics* graphics) +{ + WDL_String directory(mRvcRoot.Get()); + graphics->PromptForDirectory(directory, [this](const WDL_String&, const WDL_String& selectedDirectory) { + if (selectedDirectory.GetLength() > 0) + SetRvcRoot(selectedDirectory.Get()); + }); +} + +void RVCRealtime::ChoosePython(IGraphics* graphics) +{ + WDL_String fileName, path; + const std::string currentDirectory = ParentDirectory(mPythonPath.Get()); + if (!currentDirectory.empty()) + path.Set(currentDirectory.c_str()); + else if (mRvcRoot.GetLength() > 0) + path.Set(JoinPath(mRvcRoot.Get(), "runtime").c_str()); + graphics->PromptForFile(fileName, path, EFileAction::Open, "exe", + [this](const WDL_String& selected, const WDL_String&) { + if (selected.GetLength() > 0) + SetPythonPath(selected.Get()); + }); +} + +void RVCRealtime::ChooseModel(IGraphics* graphics) +{ + WDL_String fileName, path(mModelBrowseDirectory.Get()); + if (path.GetLength() == 0) + path.Set(mRvcRoot.Get()); + graphics->PromptForFile(fileName, path, EFileAction::Open, "pth", + [this](const WDL_String& selected, const WDL_String& selectedDirectory) { + if (selected.GetLength() > 0) { + mModelBrowseDirectory.Set(selectedDirectory.Get()); + SetModelPath(selected.Get()); + } + }); +} + +void RVCRealtime::ChooseIndex(IGraphics* graphics) +{ + WDL_String fileName, path(mIndexBrowseDirectory.Get()); + if (path.GetLength() == 0) + path.Set(mRvcRoot.Get()); + graphics->PromptForFile(fileName, path, EFileAction::Open, "index", + [this](const WDL_String& selected, const WDL_String& selectedDirectory) { + if (selected.GetLength() > 0) { + mIndexBrowseDirectory.Set(selectedDirectory.Get()); + SetIndexPath(selected.Get()); + } + }); +} + +void RVCRealtime::SetRvcRoot(const char* path) +{ + const std::string root = TrimTrailingSeparators(path != nullptr ? path : ""); + const std::string detectedPython = JoinPath(root, "runtime\\python.exe"); + const bool pythonDetected = PathIsFile(detectedPython); + { + std::lock_guard lock(mStateMutex); + mRvcRoot.Set(root.c_str()); + mPythonPath.Set(pythonDetected ? detectedPython.c_str() : ""); + mModelBrowseDirectory.Set(root.c_str()); + mIndexBrowseDirectory.Set(root.c_str()); + mValidationMessage.Set(pythonDetected ? "" : "runtime\\python.exe not found; select Python manually."); + } + mWorker.setPath(rvc::kStateRvcRoot, root.c_str()); + mWorker.setPath(rvc::kStatePythonPath, pythonDetected ? detectedPython.c_str() : ""); + StopEngineForPathChange(); + UpdateFileLabels(); +} + +void RVCRealtime::SetPythonPath(const char* path) +{ + const char* value = path != nullptr ? path : ""; + { + std::lock_guard lock(mStateMutex); + mPythonPath.Set(value); + mValidationMessage.Set(""); + } + mWorker.setPath(rvc::kStatePythonPath, value); + StopEngineForPathChange(); + UpdateFileLabels(); +} + +void RVCRealtime::SetModelPath(const char* path) +{ + { + std::lock_guard lock(mStateMutex); + mModelPath.Set(path); + } + mWorker.setPath(rvc::kStateModelPath, path); + StopEngineForPathChange(); + UpdateFileLabels(); +} + +void RVCRealtime::SetIndexPath(const char* path) +{ + { + std::lock_guard lock(mStateMutex); + mIndexPath.Set(path); + } + mWorker.setPath(rvc::kStateIndexPath, path); + StopEngineForPathChange(); + UpdateFileLabels(); +} + +void RVCRealtime::StopEngineForPathChange() +{ + mWorker.setEnabled(false); + if (GetParam(kEngine)->Bool()) { + GetParam(kEngine)->Set(0.0); + SendParameterValueFromAPI(kEngine, 0.0, false); + } +} + +bool RVCRealtime::ValidateConfiguration(std::string& error) const +{ + std::string root, python, model, index; + { + std::lock_guard lock(mStateMutex); + root = mRvcRoot.Get(); + python = mPythonPath.Get(); + model = mModelPath.Get(); + index = mIndexPath.Get(); + } + if (root.empty()) { error = "Select the RVC package root."; return false; } + if (!PathIsDirectory(root)) { error = "RVC root folder does not exist."; return false; } + if (!PathIsFile(JoinPath(root, "infer\\rtrvc.py"))) { error = "RVC source not found: infer\\rtrvc.py."; return false; } + if (!PathIsFile(JoinPath(root, "configs\\config.py"))) { error = "RVC source not found: configs\\config.py."; return false; } + if (python.empty()) { error = "runtime\\python.exe not found; select Python manually."; return false; } + if (!PathIsFile(python)) { error = "Selected Python executable does not exist."; return false; } + if (!Is64BitExecutable(python)) { error = "Selected Python must be a 64-bit executable."; return false; } + if (model.empty()) { error = "Select an RVC .pth model."; return false; } + if (!PathIsFile(model)) { error = "Selected model file does not exist."; return false; } + if (!index.empty() && !PathIsFile(index)) { error = "Selected index file does not exist."; return false; } + error.clear(); + return true; +} + +void RVCRealtime::LoadUserConfiguration() +{ + const std::string root = ReadSetting(L"RvcRoot"); + const std::string python = ReadSetting(L"PythonPath"); + const std::string model = ReadSetting(L"ModelPath"); + const std::string index = ReadSetting(L"IndexPath"); + if (!root.empty()) mRvcRoot.Set(root.c_str()); + if (!python.empty()) mPythonPath.Set(python.c_str()); + if (!model.empty()) mModelPath.Set(model.c_str()); + if (!index.empty()) mIndexPath.Set(index.c_str()); +} + +void RVCRealtime::SaveUserConfiguration() const +{ + std::string root, python, model, index; + { + std::lock_guard lock(mStateMutex); + root = mRvcRoot.Get(); + python = mPythonPath.Get(); + model = mModelPath.Get(); + index = mIndexPath.Get(); + } + const std::wstring settingsPath = SettingsFilePath(true); + WriteSetting(settingsPath, L"RvcRoot", root); + WriteSetting(settingsPath, L"PythonPath", python); + WriteSetting(settingsPath, L"ModelPath", model); + WriteSetting(settingsPath, L"IndexPath", index); +} + +void RVCRealtime::UpdateFileLabels() +{ +#if IPLUG_EDITOR + if (GetUI() == nullptr) + return; + std::lock_guard lock(mStateMutex); + if (auto* root = GetUI()->GetControlWithTag(kCtrlRvcRoot)) + root->As()->SetStr(mRvcRoot.Get()); + if (auto* python = GetUI()->GetControlWithTag(kCtrlPythonPath)) + python->As()->SetStr(mPythonPath.Get()); + if (auto* model = GetUI()->GetControlWithTag(kCtrlModelName)) + model->As()->SetStr(mModelPath.get_filepart()); + if (auto* index = GetUI()->GetControlWithTag(kCtrlIndexName)) + index->As()->SetStr(mIndexPath.get_filepart()); +#endif +} diff --git a/RVCRealtimeVST/src/RVCRealtime.h b/RVCRealtimeVST/src/RVCRealtime.h new file mode 100644 index 0000000..8410c49 --- /dev/null +++ b/RVCRealtimeVST/src/RVCRealtime.h @@ -0,0 +1,96 @@ +#pragma once + +#include "IPlug_include_in_plug_hdr.h" + +#include "RvcParameters.hpp" +#include "WorkerClient.hpp" + +#include +#include +#include +#include + +const int kNumPresets = 1; + +enum EParams { + kEngine = 0, + kPitch, + kFormant, + kIndexRate, + kRmsMix, + kThreshold, + kBlockMs, + kCrossfadeMs, + kExtraMs, + kF0Method, + kDryWet, + kOutputGain, + kNumParams +}; + +enum EControlTags { + kCtrlStatus = 100, + kCtrlPerformance, + kCtrlStatusDetail, + kCtrlRvcRoot, + kCtrlPythonPath, + kCtrlModelName, + kCtrlIndexName +}; + +using namespace iplug; +using namespace igraphics; + +class RVCRealtime final : public Plugin { +public: + RVCRealtime(const InstanceInfo& info); + +#if IPLUG_DSP + void ProcessBlock(sample** inputs, sample** outputs, int nFrames) override; + void OnReset() override; + void OnParamChange(int paramIdx) override; +#endif + + void OnIdle() override; + void OnUIOpen() override; + bool SerializeState(IByteChunk& chunk) const override; + int UnserializeState(const IByteChunk& chunk, int startPos) override; + +private: + void SyncParametersToWorker(); + int CalculateLatencyFrames(double blockMs) const; + void ResizeBuffers(int blockSize, double sampleRate); + void ChooseRvcRoot(IGraphics* graphics); + void ChoosePython(IGraphics* graphics); + void ChooseModel(IGraphics* graphics); + void ChooseIndex(IGraphics* graphics); + void SetRvcRoot(const char* path); + void SetPythonPath(const char* path); + void SetModelPath(const char* path); + void SetIndexPath(const char* path); + void UpdateFileLabels(); + void StopEngineForPathChange(); + bool ValidateConfiguration(std::string& error) const; + void LoadUserConfiguration(); + void SaveUserConfiguration() const; + + rvc::WorkerClient mWorker; + std::vector mMonoInput; + std::vector mWetOutput; + std::vector mDryDelay; + int mDryWritePosition = 0; + std::atomic mTargetDelayFrames {12480}; + float mActiveBlend = 0.0f; + + mutable std::mutex mStateMutex; + WDL_String mModelPath {RVC_DEFAULT_MODEL}; + WDL_String mIndexPath {RVC_DEFAULT_INDEX}; + WDL_String mRvcRoot {RVC_DEFAULT_ROOT}; + WDL_String mPythonPath {RVC_DEFAULT_PYTHON}; + WDL_String mModelBrowseDirectory; + WDL_String mIndexBrowseDirectory; + WDL_String mValidationMessage; + + RVCRealtime(const RVCRealtime&) = delete; + RVCRealtime& operator=(const RVCRealtime&) = delete; +}; diff --git a/RVCRealtimeVST/src/RvcParameters.hpp b/RVCRealtimeVST/src/RvcParameters.hpp new file mode 100644 index 0000000..e9189af --- /dev/null +++ b/RVCRealtimeVST/src/RvcParameters.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +namespace rvc { + +enum ParameterId : uint32_t { + kParamEngine = 0, + kParamPitch, + kParamFormant, + kParamIndexRate, + kParamRmsMix, + kParamThreshold, + kParamBlockMs, + kParamCrossfadeMs, + kParamExtraMs, + kParamF0Method, + kParamDryWet, + kParamOutputGain, + kParamStatus, + kParamInferMs, + kParamDroppedBlocks, + kParameterCount +}; + +enum StateId : uint32_t { + kStateModelPath = 0, + kStateIndexPath, + kStateRvcRoot, + kStatePythonPath, + kStateCount +}; + +enum WorkerStatus : int { + kStatusOff = 0, + kStatusStarting = 1, + kStatusLoading = 2, + kStatusReady = 3, + kStatusError = 4 +}; + +namespace ids { +inline constexpr const char* engine = "engine"; +inline constexpr const char* pitch = "pitch"; +inline constexpr const char* formant = "formant"; +inline constexpr const char* indexRate = "index_rate"; +inline constexpr const char* rmsMix = "rms_mix"; +inline constexpr const char* threshold = "threshold"; +inline constexpr const char* blockMs = "block_ms"; +inline constexpr const char* crossfadeMs = "crossfade_ms"; +inline constexpr const char* extraMs = "extra_ms"; +inline constexpr const char* f0Method = "f0_method"; +inline constexpr const char* dryWet = "mix"; +inline constexpr const char* outputGain = "output_gain"; +} // namespace ids + +inline const char* stateKey(const StateId id) +{ + switch (id) { + case kStateModelPath: return "modelPath"; + case kStateIndexPath: return "indexPath"; + case kStateRvcRoot: return "rvcRoot"; + case kStatePythonPath: return "pythonPath"; + default: return ""; + } +} + +} // namespace rvc diff --git a/RVCRealtimeVST/src/SpscRing.hpp b/RVCRealtimeVST/src/SpscRing.hpp new file mode 100644 index 0000000..ed08869 --- /dev/null +++ b/RVCRealtimeVST/src/SpscRing.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace rvc { + +class SpscFloatRing { +public: + explicit SpscFloatRing(const std::size_t requestedCapacity) + { + std::size_t capacity = 1; + while (capacity < requestedCapacity) + capacity <<= 1; + capacity_ = capacity; + mask_ = capacity - 1; + data_.reset(new float[capacity]); + std::memset(data_.get(), 0, capacity * sizeof(float)); + } + + std::size_t readable() const noexcept + { + const auto write = write_.load(std::memory_order_acquire); + const auto read = read_.load(std::memory_order_acquire); + return write - read; + } + + std::size_t writable() const noexcept + { + return capacity_ - readable(); + } + + std::size_t push(const float* src, std::size_t count) noexcept + { + const auto write = write_.load(std::memory_order_relaxed); + const auto read = read_.load(std::memory_order_acquire); + count = std::min(count, capacity_ - (write - read)); + if (count == 0) + return 0; + + const std::size_t pos = write & mask_; + const std::size_t first = std::min(count, capacity_ - pos); + std::memcpy(data_.get() + pos, src, first * sizeof(float)); + if (count > first) + std::memcpy(data_.get(), src + first, (count - first) * sizeof(float)); + write_.store(write + count, std::memory_order_release); + return count; + } + + std::size_t pushZeros(std::size_t count) noexcept + { + static constexpr float zeros[1024] = {}; + std::size_t total = 0; + while (total < count) { + const std::size_t chunk = std::min(1024, count - total); + const std::size_t written = push(zeros, chunk); + total += written; + if (written != chunk) + break; + } + return total; + } + + std::size_t pop(float* dst, std::size_t count) noexcept + { + const auto read = read_.load(std::memory_order_relaxed); + const auto write = write_.load(std::memory_order_acquire); + count = std::min(count, write - read); + if (count == 0) + return 0; + + const std::size_t pos = read & mask_; + const std::size_t first = std::min(count, capacity_ - pos); + std::memcpy(dst, data_.get() + pos, first * sizeof(float)); + if (count > first) + std::memcpy(dst + first, data_.get(), (count - first) * sizeof(float)); + read_.store(read + count, std::memory_order_release); + return count; + } + + void resetUnsafe() noexcept + { + read_.store(0, std::memory_order_relaxed); + write_.store(0, std::memory_order_relaxed); + } + +private: + std::unique_ptr data_; + std::size_t capacity_ = 0; + std::size_t mask_ = 0; + alignas(64) std::atomic write_ {0}; + alignas(64) std::atomic read_ {0}; +}; + +} // namespace rvc diff --git a/RVCRealtimeVST/src/WorkerClient.cpp b/RVCRealtimeVST/src/WorkerClient.cpp new file mode 100644 index 0000000..08d22e0 --- /dev/null +++ b/RVCRealtimeVST/src/WorkerClient.cpp @@ -0,0 +1,587 @@ +#include "WorkerClient.hpp" +#include "config.h" + +#if !defined(_WIN32) +#error RVC Realtime worker bridge currently targets Windows. +#endif + +#include + +#include +#include +#include +#include +#include +#include + +namespace rvc { +namespace { + +constexpr uint32_t kMagic = 0x50564352; // RVCP +constexpr uint32_t kProtocolVersion = 1; +constexpr uint32_t kHeaderBytes = 4096; +constexpr uint32_t kMaxFrames = 131072; +constexpr uint32_t kMapBytes = kHeaderBytes + kMaxFrames * sizeof(float) * 2; +constexpr uint32_t kInputOffset = kHeaderBytes; +constexpr uint32_t kOutputOffset = kHeaderBytes + kMaxFrames * sizeof(float); +constexpr uint32_t kStatusTextOffset = 128; +constexpr uint32_t kStatusTextBytes = 512; + +template +void writeAt(void* const base, const std::size_t offset, const T value) +{ + std::memcpy(static_cast(base) + offset, &value, sizeof(T)); +} + +template +T readAt(const void* const base, const std::size_t offset) +{ + T value {}; + std::memcpy(&value, static_cast(base) + offset, sizeof(T)); + return value; +} + +std::wstring utf8ToWide(const std::string& text) +{ + if (text.empty()) + return {}; + const int length = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0); + std::wstring result(static_cast(length), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, result.data(), length); + if (!result.empty() && result.back() == L'\0') + result.pop_back(); + return result; +} + +std::string wideToUtf8(const std::wstring& text) +{ + if (text.empty()) + return {}; + const int length = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, nullptr, 0, nullptr, nullptr); + std::string result(static_cast(length), '\0'); + WideCharToMultiByte(CP_UTF8, 0, text.c_str(), -1, result.data(), length, nullptr, nullptr); + if (!result.empty() && result.back() == '\0') + result.pop_back(); + return result; +} + +std::wstring moduleDirectory() +{ + static int moduleAnchor = 0; + HMODULE module = nullptr; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&moduleAnchor), &module)) + return {}; + std::vector path(32768, L'\0'); + const DWORD length = GetModuleFileNameW(module, path.data(), static_cast(path.size())); + if (length == 0 || length >= path.size()) + return {}; + std::wstring result(path.data(), length); + const std::size_t separator = result.find_last_of(L"\\/"); + return separator == std::wstring::npos ? std::wstring() : result.substr(0, separator); +} + +bool isFile(const std::wstring& path) +{ + const DWORD attributes = GetFileAttributesW(path.c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +std::wstring workerScriptPath() +{ + const std::wstring directory = moduleDirectory(); + if (directory.empty()) + return {}; + const std::wstring relativeWorker = utf8ToWide(RVC_WORKER_RELATIVE_PATH); + const std::wstring vst2Resources = utf8ToWide(RVC_VST2_RESOURCES_DIR); + + const std::wstring vst3Path = directory + L"\\..\\Resources\\" + relativeWorker; + if (isFile(vst3Path)) + return vst3Path; + + const std::wstring vst2Path = directory + L"\\" + vst2Resources + L"\\" + relativeWorker; + if (isFile(vst2Path)) + return vst2Path; + + return {}; +} + +bool ensureDirectory(const std::wstring& path, std::string& error) +{ + if (CreateDirectoryW(path.c_str(), nullptr) != FALSE) + return true; + const DWORD code = GetLastError(); + if (code == ERROR_ALREADY_EXISTS) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + return true; + } + error = "Cannot create " + wideToUtf8(path) + " (Windows error " + std::to_string(code) + ")"; + return false; +} + +std::wstring temporaryLogDirectory(std::string& error) +{ + std::vector temporaryPath(32768, L'\0'); + const DWORD length = GetTempPathW(static_cast(temporaryPath.size()), temporaryPath.data()); + if (length == 0 || length >= temporaryPath.size()) { + error = "Windows temporary directory is unavailable (Windows error " + std::to_string(GetLastError()) + ")"; + return {}; + } + + std::wstring productDirectory(temporaryPath.data(), length); + if (!productDirectory.empty() && productDirectory.back() != L'\\') + productDirectory += L'\\'; + productDirectory += L"RVCRealtime"; + if (!ensureDirectory(productDirectory, error)) + return {}; + + const std::wstring logDirectory = productDirectory + L"\\logs"; + if (!ensureDirectory(logDirectory, error)) + return {}; + return logDirectory; +} + +std::string jsonEscape(const std::string& text) +{ + std::ostringstream out; + for (const unsigned char ch : text) { + switch (ch) { + case '\\': out << "\\\\"; break; + case '"': out << "\\\""; break; + case '\n': out << "\\n"; break; + case '\r': out << "\\r"; break; + case '\t': out << "\\t"; break; + default: + if (ch < 0x20) + out << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(ch); + else + out << static_cast(ch); + } + } + return out.str(); +} + +std::string quoteArg(const std::string& text) +{ + return "\"" + text + "\""; +} + +} // namespace + +struct WorkerClient::Ipc { + HANDLE mapping = nullptr; + HANDLE requestEvent = nullptr; + HANDLE responseEvent = nullptr; + HANDLE process = nullptr; + HANDLE processThread = nullptr; + void* view = nullptr; + std::string mapName; + std::string requestName; + std::string responseName; + std::string configPath; + uint32_t sequence = 0; + + ~Ipc() + { + if (view != nullptr) + UnmapViewOfFile(view); + if (mapping != nullptr) + CloseHandle(mapping); + if (requestEvent != nullptr) + CloseHandle(requestEvent); + if (responseEvent != nullptr) + CloseHandle(responseEvent); + if (processThread != nullptr) + CloseHandle(processThread); + if (process != nullptr) + CloseHandle(process); + } +}; + +WorkerClient::WorkerClient() +{ + parameters_[kParamPitch].store(12.0f); + parameters_[kParamFormant].store(0.0f); + parameters_[kParamIndexRate].store(0.0f); + parameters_[kParamRmsMix].store(0.5f); + parameters_[kParamThreshold].store(-60.0f); + parameters_[kParamBlockMs].store(130.0f); + parameters_[kParamCrossfadeMs].store(80.0f); + parameters_[kParamExtraMs].store(2000.0f); + parameters_[kParamF0Method].store(0.0f); + + paths_.model = RVC_DEFAULT_MODEL; + paths_.index = RVC_DEFAULT_INDEX; + paths_.rvcRoot = RVC_DEFAULT_ROOT; + paths_.python = RVC_DEFAULT_PYTHON; + + thread_ = std::thread(&WorkerClient::threadMain, this); +} + +WorkerClient::~WorkerClient() +{ + stopRequested_.store(true, std::memory_order_release); + if (thread_.joinable()) + thread_.join(); +} + +void WorkerClient::setEnabled(const bool enabled) noexcept +{ + if (enabled_.exchange(enabled, std::memory_order_acq_rel) != enabled) + configVersion_.fetch_add(1, std::memory_order_release); +} + +void WorkerClient::setSampleRate(const double sampleRate) noexcept +{ + if (std::abs(sampleRate_.load(std::memory_order_relaxed) - sampleRate) > 0.5) { + sampleRate_.store(sampleRate, std::memory_order_relaxed); + configVersion_.fetch_add(1, std::memory_order_release); + } +} + +void WorkerClient::setParameter(const ParameterId id, const float value) noexcept +{ + if (id >= kParameterCount) + return; + const float old = parameters_[id].exchange(value, std::memory_order_relaxed); + if ((id == kParamBlockMs || id == kParamCrossfadeMs || id == kParamExtraMs) && std::abs(old - value) > 0.01f) + configVersion_.fetch_add(1, std::memory_order_release); +} + +void WorkerClient::setPath(const StateId id, const char* const value) +{ + if (value == nullptr) + return; + std::lock_guard lock(pathsMutex_); + switch (id) { + case kStateModelPath: paths_.model = value; break; + case kStateIndexPath: paths_.index = value; break; + case kStateRvcRoot: paths_.rvcRoot = value; break; + case kStatePythonPath: paths_.python = value; break; + default: return; + } + configVersion_.fetch_add(1, std::memory_order_release); +} + +std::size_t WorkerClient::pushInput(const float* const samples, const std::size_t count) noexcept +{ + if (!isReady()) + return 0; + const std::size_t pushed = inputRing_.push(samples, count); + if (pushed != count) + droppedBlocks_.fetch_add(1, std::memory_order_relaxed); + return pushed; +} + +std::size_t WorkerClient::popOutput(float* const samples, const std::size_t count) noexcept +{ + if (!isReady()) + return 0; + return outputRing_.pop(samples, count); +} + +std::string WorkerClient::statusText() const +{ + std::lock_guard lock(statusTextMutex_); + return statusText_; +} + +WorkerClient::Paths WorkerClient::pathsSnapshot() const +{ + std::lock_guard lock(pathsMutex_); + return paths_; +} + +void WorkerClient::setStatus(const int status, const std::string& text) +{ + status_.store(status, std::memory_order_release); + std::lock_guard lock(statusTextMutex_); + statusText_ = text; +} + +uint32_t WorkerClient::calculateBlockFrames() const noexcept +{ + const double sampleRate = sampleRate_.load(std::memory_order_relaxed); + const double zc = std::max(1.0, std::floor(sampleRate / 100.0)); + const double seconds = parameters_[kParamBlockMs].load(std::memory_order_relaxed) / 1000.0; + const auto frames = static_cast(std::round(seconds * sampleRate / zc) * zc); + return std::clamp(frames, static_cast(zc), kMaxFrames); +} + +std::string WorkerClient::writeWorkerConfig(const Paths& paths, std::string& error) const +{ + const DWORD pid = GetCurrentProcessId(); + const std::wstring logDirectory = temporaryLogDirectory(error); + if (logDirectory.empty()) + return {}; + + std::wostringstream name; + name << logDirectory << L"\\instance_" << pid << L"_" << GetTickCount64() << L".json"; + const std::wstring configPath = name.str(); + HANDLE file = CreateFileW(configPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, nullptr); + if (file == INVALID_HANDLE_VALUE) { + error = "Cannot create " + wideToUtf8(configPath) + " (Windows error " + + std::to_string(GetLastError()) + ")"; + return {}; + } + + std::ostringstream json; + json << "{\n" + << " \"rvc_root\": \"" << jsonEscape(paths.rvcRoot) << "\",\n" + << " \"model_path\": \"" << jsonEscape(paths.model) << "\",\n" + << " \"index_path\": \"" << jsonEscape(paths.index) << "\",\n" + << " \"sample_rate\": " << static_cast(sampleRate_.load()) << ",\n" + << " \"block_ms\": " << parameters_[kParamBlockMs].load() << ",\n" + << " \"crossfade_ms\": " << parameters_[kParamCrossfadeMs].load() << ",\n" + << " \"extra_ms\": " << parameters_[kParamExtraMs].load() << "\n" + << "}\n"; + const std::string contents = json.str(); + DWORD bytesWritten = 0; + const BOOL written = WriteFile(file, contents.data(), static_cast(contents.size()), &bytesWritten, nullptr); + const DWORD writeError = written != FALSE ? ERROR_SUCCESS : GetLastError(); + CloseHandle(file); + if (written == FALSE || bytesWritten != static_cast(contents.size())) { + DeleteFileW(configPath.c_str()); + error = "Cannot write " + wideToUtf8(configPath) + " (Windows error " + + std::to_string(writeError) + ")"; + return {}; + } + error.clear(); + return wideToUtf8(configPath); +} + +bool WorkerClient::launchWorker(const Paths& paths, const uint64_t) +{ + ipc_ = std::make_unique(); + const DWORD pid = GetCurrentProcessId(); + static std::atomic instanceCounter {0}; + const uint32_t instance = instanceCounter.fetch_add(1); + const std::string id = std::to_string(pid) + "_" + std::to_string(instance) + "_" + std::to_string(GetTickCount64()); + ipc_->mapName = "Local\\RVCVST_" + id + "_map"; + ipc_->requestName = "Local\\RVCVST_" + id + "_request"; + ipc_->responseName = "Local\\RVCVST_" + id + "_response"; + + const std::wstring mapName = utf8ToWide(ipc_->mapName); + const std::wstring requestName = utf8ToWide(ipc_->requestName); + const std::wstring responseName = utf8ToWide(ipc_->responseName); + ipc_->mapping = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, kMapBytes, mapName.c_str()); + ipc_->requestEvent = CreateEventW(nullptr, FALSE, FALSE, requestName.c_str()); + ipc_->responseEvent = CreateEventW(nullptr, FALSE, FALSE, responseName.c_str()); + if (ipc_->mapping == nullptr || ipc_->requestEvent == nullptr || ipc_->responseEvent == nullptr) { + setStatus(kStatusError, "IPC initialization failed"); + stopWorker(); + return false; + } + + ipc_->view = MapViewOfFile(ipc_->mapping, FILE_MAP_ALL_ACCESS, 0, 0, kMapBytes); + if (ipc_->view == nullptr) { + setStatus(kStatusError, "Shared memory mapping failed"); + stopWorker(); + return false; + } + std::memset(ipc_->view, 0, kMapBytes); + writeAt(ipc_->view, 0, kMagic); + writeAt(ipc_->view, 4, kProtocolVersion); + writeAt(ipc_->view, 8, kStatusStarting); + + std::string configError; + ipc_->configPath = writeWorkerConfig(paths, configError); + if (ipc_->configPath.empty()) { + setStatus(kStatusError, configError.empty() ? "Could not write worker configuration" : configError); + stopWorker(); + return false; + } + + const std::wstring workerScript = workerScriptPath(); + if (workerScript.empty()) { + setStatus(kStatusError, "Plugin worker resource is missing"); + stopWorker(); + return false; + } + std::string command = quoteArg(paths.python) + " -I " + quoteArg(wideToUtf8(workerScript)) + + " --map " + quoteArg(ipc_->mapName) + + " --request " + quoteArg(ipc_->requestName) + + " --response " + quoteArg(ipc_->responseName) + + " --config " + quoteArg(ipc_->configPath); + std::wstring commandWide = utf8ToWide(command); + std::vector commandBuffer(commandWide.begin(), commandWide.end()); + commandBuffer.push_back(L'\0'); + + STARTUPINFOW startup {}; + startup.cb = sizeof(startup); + SECURITY_ATTRIBUTES security {}; + security.nLength = sizeof(security); + security.bInheritHandle = TRUE; + const std::wstring logPath = utf8ToWide(ipc_->configPath + ".process.log"); + HANDLE logFile = CreateFileW(logPath.c_str(), GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, + &security, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (logFile != INVALID_HANDLE_VALUE) { + startup.dwFlags |= STARTF_USESTDHANDLES; + startup.hStdOutput = logFile; + startup.hStdError = logFile; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + } + PROCESS_INFORMATION process {}; + const std::wstring cwd = utf8ToWide(paths.rvcRoot); + const BOOL inheritHandles = logFile != INVALID_HANDLE_VALUE; + const std::wstring application = utf8ToWide(paths.python); + const BOOL launched = CreateProcessW(application.c_str(), commandBuffer.data(), nullptr, nullptr, inheritHandles, + CREATE_NO_WINDOW, nullptr, cwd.c_str(), &startup, &process); + if (logFile != INVALID_HANDLE_VALUE) + CloseHandle(logFile); + if (!launched) { + setStatus(kStatusError, "Python worker launch failed"); + stopWorker(); + return false; + } + ipc_->process = process.hProcess; + ipc_->processThread = process.hThread; + setStatus(kStatusLoading, "Loading RVC model"); + return true; +} + +void WorkerClient::stopWorker() +{ + ready_.store(false, std::memory_order_release); + if (ipc_ && ipc_->view != nullptr && ipc_->requestEvent != nullptr) { + writeAt(ipc_->view, 8, -2); + SetEvent(ipc_->requestEvent); + } + if (ipc_ && ipc_->process != nullptr) { + if (WaitForSingleObject(ipc_->process, 1200) == WAIT_TIMEOUT) + TerminateProcess(ipc_->process, 0); + } + ipc_.reset(); +} + +bool WorkerClient::processOneBlock() +{ + if (!ipc_ || !ipc_->view) + return false; + const uint32_t frames = blockFrames_.load(std::memory_order_relaxed); + if (inputRing_.readable() < frames) + return true; + if (requestBuffer_.size() < frames) { + requestBuffer_.resize(frames); + responseBuffer_.resize(frames); + } + if (inputRing_.pop(requestBuffer_.data(), frames) != frames) + return true; + + const uint32_t sequence = ++ipc_->sequence; + writeAt(ipc_->view, 12, sequence); + writeAt(ipc_->view, 20, frames); + writeAt(ipc_->view, 24, static_cast(sampleRate_.load(std::memory_order_relaxed))); + writeAt(ipc_->view, 32, parameters_[kParamPitch].load(std::memory_order_relaxed)); + writeAt(ipc_->view, 36, parameters_[kParamFormant].load(std::memory_order_relaxed)); + writeAt(ipc_->view, 40, parameters_[kParamIndexRate].load(std::memory_order_relaxed)); + writeAt(ipc_->view, 44, parameters_[kParamRmsMix].load(std::memory_order_relaxed)); + writeAt(ipc_->view, 48, parameters_[kParamThreshold].load(std::memory_order_relaxed)); + writeAt(ipc_->view, 64, static_cast(std::round(parameters_[kParamF0Method].load(std::memory_order_relaxed)))); + std::memcpy(static_cast(ipc_->view) + kInputOffset, requestBuffer_.data(), frames * sizeof(float)); + SetEvent(ipc_->requestEvent); + + const DWORD timeout = std::max(5000, static_cast(parameters_[kParamBlockMs].load() * 8.0f)); + const DWORD wait = WaitForSingleObject(ipc_->responseEvent, timeout); + if (wait != WAIT_OBJECT_0) { + setStatus(kStatusError, "RVC inference timed out"); + return false; + } + if (readAt(ipc_->view, 16) != sequence) { + droppedBlocks_.fetch_add(1, std::memory_order_relaxed); + return true; + } + + const int workerState = readAt(ipc_->view, 8); + if (workerState < 0) { + const char* const text = reinterpret_cast(static_cast(ipc_->view) + kStatusTextOffset); + setStatus(kStatusError, text[0] != '\0' ? text : "Worker error"); + return false; + } + + inferMs_.store(readAt(ipc_->view, 56), std::memory_order_relaxed); + std::memcpy(responseBuffer_.data(), static_cast(ipc_->view) + kOutputOffset, frames * sizeof(float)); + if (outputRing_.push(responseBuffer_.data(), frames) != frames) + droppedBlocks_.fetch_add(1, std::memory_order_relaxed); + return true; +} + +void WorkerClient::threadMain() +{ + uint64_t activeVersion = 0; + while (!stopRequested_.load(std::memory_order_acquire)) { + const bool enabled = enabled_.load(std::memory_order_acquire); + const uint64_t requestedVersion = configVersion_.load(std::memory_order_acquire); + if (!enabled) { + if (ipc_) + stopWorker(); + setStatus(kStatusOff, "Off"); + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + continue; + } + + if (!ipc_ || activeVersion != requestedVersion) { + stopWorker(); + const Paths paths = pathsSnapshot(); + if (paths.model.empty() || paths.rvcRoot.empty() || paths.python.empty()) { + setStatus(kStatusError, "Select a model and RVC runtime"); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + continue; + } + activeVersion = requestedVersion; + blockFrames_.store(calculateBlockFrames(), std::memory_order_relaxed); + latencyFrames_.store(blockFrames_.load() * 2, std::memory_order_relaxed); + droppedBlocks_.store(0, std::memory_order_relaxed); + inferMs_.store(0.0f, std::memory_order_relaxed); + if (!launchWorker(paths, activeVersion)) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + continue; + } + } + + if (!ready_.load(std::memory_order_acquire)) { + const int workerState = readAt(ipc_->view, 8); + const char* const workerText = reinterpret_cast(static_cast(ipc_->view) + kStatusTextOffset); + if (workerState == kStatusReady) { + inputRing_.resetUnsafe(); + outputRing_.resetUnsafe(); + outputRing_.pushZeros(latencyFrames_.load(std::memory_order_relaxed)); + setStatus(kStatusReady, workerText[0] != '\0' ? workerText : "Ready"); + ready_.store(true, std::memory_order_release); + } else if (workerState < 0) { + setStatus(kStatusError, workerText[0] != '\0' ? workerText : "Model load failed"); + stopWorker(); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + continue; + } else if (workerText[0] != '\0') { + setStatus(kStatusLoading, workerText); + } + if (WaitForSingleObject(ipc_->process, 0) == WAIT_OBJECT_0) { + if (workerState >= 0) { + DWORD exitCode = 0; + GetExitCodeProcess(ipc_->process, &exitCode); + setStatus(kStatusError, "Python worker exited with code " + std::to_string(exitCode)); + } + stopWorker(); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + continue; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + continue; + } + + if (!processOneBlock()) { + ready_.store(false, std::memory_order_release); + stopWorker(); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + continue; + } + if (inputRing_.readable() < blockFrames_.load(std::memory_order_relaxed)) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + stopWorker(); +} + +} // namespace rvc diff --git a/RVCRealtimeVST/src/WorkerClient.hpp b/RVCRealtimeVST/src/WorkerClient.hpp new file mode 100644 index 0000000..eab54f5 --- /dev/null +++ b/RVCRealtimeVST/src/WorkerClient.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include "RvcParameters.hpp" +#include "SpscRing.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace rvc { + +class WorkerClient { +public: + WorkerClient(); + ~WorkerClient(); + + WorkerClient(const WorkerClient&) = delete; + WorkerClient& operator=(const WorkerClient&) = delete; + + void setEnabled(bool enabled) noexcept; + void setSampleRate(double sampleRate) noexcept; + void setParameter(ParameterId id, float value) noexcept; + void setPath(StateId id, const char* value); + + std::size_t pushInput(const float* samples, std::size_t count) noexcept; + std::size_t popOutput(float* samples, std::size_t count) noexcept; + + bool isReady() const noexcept { return ready_.load(std::memory_order_acquire); } + int status() const noexcept { return status_.load(std::memory_order_acquire); } + float inferMs() const noexcept { return inferMs_.load(std::memory_order_relaxed); } + float droppedBlocks() const noexcept { return static_cast(droppedBlocks_.load(std::memory_order_relaxed)); } + uint32_t blockFrames() const noexcept { return blockFrames_.load(std::memory_order_relaxed); } + uint32_t latencyFrames() const noexcept { return latencyFrames_.load(std::memory_order_relaxed); } + std::string statusText() const; + +private: + struct Paths { + std::string model; + std::string index; + std::string rvcRoot; + std::string python; + }; + + struct Ipc; + + void threadMain(); + bool launchWorker(const Paths& paths, uint64_t version); + void stopWorker(); + bool processOneBlock(); + Paths pathsSnapshot() const; + void setStatus(int status, const std::string& text); + uint32_t calculateBlockFrames() const noexcept; + std::string writeWorkerConfig(const Paths& paths, std::string& error) const; + + static constexpr std::size_t kRingCapacity = 1u << 20; + SpscFloatRing inputRing_ {kRingCapacity}; + SpscFloatRing outputRing_ {kRingCapacity}; + + std::array, kParameterCount> parameters_ {}; + std::atomic enabled_ {false}; + std::atomic stopRequested_ {false}; + std::atomic ready_ {false}; + std::atomic status_ {kStatusOff}; + std::atomic inferMs_ {0.0f}; + std::atomic droppedBlocks_ {0}; + std::atomic blockFrames_ {6240}; + std::atomic latencyFrames_ {12480}; + std::atomic sampleRate_ {48000.0}; + std::atomic configVersion_ {1}; + + mutable std::mutex pathsMutex_; + Paths paths_; + mutable std::mutex statusTextMutex_; + std::string statusText_ {"Off"}; + + std::unique_ptr ipc_; + std::vector requestBuffer_; + std::vector responseBuffer_; + std::thread thread_; +}; + +} // namespace rvc diff --git a/RVCRealtimeVST/third_party/iPlug2 b/RVCRealtimeVST/third_party/iPlug2 new file mode 160000 index 0000000..5c2df9d --- /dev/null +++ b/RVCRealtimeVST/third_party/iPlug2 @@ -0,0 +1 @@ +Subproject commit 5c2df9dce3f5258acfeff3846a6a9563f382212c diff --git a/RVCRealtimeVST/third_party/licenses/Roboto-Apache-2.0.txt b/RVCRealtimeVST/third_party/licenses/Roboto-Apache-2.0.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/RVCRealtimeVST/third_party/licenses/Roboto-Apache-2.0.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/RVCRealtimeVST/third_party/vst2-compat/aeffectx.h b/RVCRealtimeVST/third_party/vst2-compat/aeffectx.h new file mode 100644 index 0000000..b7f5cf0 --- /dev/null +++ b/RVCRealtimeVST/third_party/vst2-compat/aeffectx.h @@ -0,0 +1,336 @@ +// Permissive compatibility names for iPlug2's VST2 adapter. +// The ABI values and layout are backed by the BSD-3-Clause clean-room SDK in +// third_party/vst2sdk. This file contains only the subset used by iPlug2. + +#pragma once + +#include "vst.h" + +#include +#include + +using VstInt16 = std::int16_t; +using VstInt32 = std::int32_t; +using VstInt64 = std::int64_t; +using VstIntPtr = std::intptr_t; + +#define VSTCALLBACK VST_FUNCTION_INTERFACE + +struct AEffect; + +using audioMasterCallback = VstIntPtr (VSTCALLBACK*)(AEffect*, VstInt32, VstInt32, VstIntPtr, void*, float); +using AEffectDispatcherProc = VstIntPtr (VSTCALLBACK*)(AEffect*, VstInt32, VstInt32, VstIntPtr, void*, float); +using AEffectProcessProc = void (VSTCALLBACK*)(AEffect*, float**, float**, VstInt32); +using AEffectProcessDoubleProc = void (VSTCALLBACK*)(AEffect*, double**, double**, VstInt32); +using AEffectSetParameterProc = void (VSTCALLBACK*)(AEffect*, VstInt32, float); +using AEffectGetParameterProc = float (VSTCALLBACK*)(AEffect*, VstInt32); + +#pragma pack(push, 8) + +struct ERect { + VstInt16 top; + VstInt16 left; + VstInt16 bottom; + VstInt16 right; +}; + +struct AEffect { + VstInt32 magic; + AEffectDispatcherProc dispatcher; + AEffectProcessProc __processDeprecated; + AEffectSetParameterProc setParameter; + AEffectGetParameterProc getParameter; + VstInt32 numPrograms; + VstInt32 numParams; + VstInt32 numInputs; + VstInt32 numOutputs; + VstInt32 flags; + VstIntPtr resvd1; + VstIntPtr resvd2; + VstInt32 initialDelay; + VstInt32 __realQualitiesDeprecated; + VstInt32 __offQualitiesDeprecated; + float __ioRatioDeprecated; + void* object; + void* user; + VstInt32 uniqueID; + VstInt32 version; + AEffectProcessProc processReplacing; + AEffectProcessDoubleProc processDoubleReplacing; + char future[56]; +}; + +struct VstEvent { + VstInt32 type; + VstInt32 byteSize; + VstInt32 deltaFrames; + VstInt32 flags; + char data[16]; +}; + +struct VstEvents { + VstInt32 numEvents; + VstIntPtr reserved; + VstEvent* events[2]; +}; + +struct VstMidiEvent { + VstInt32 type; + VstInt32 byteSize; + VstInt32 deltaFrames; + VstInt32 flags; + VstInt32 noteLength; + VstInt32 noteOffset; + char midiData[4]; + char detune; + char noteOffVelocity; + char reserved1; + char reserved2; +}; + +struct VstMidiSysexEvent { + VstInt32 type; + VstInt32 byteSize; + VstInt32 deltaFrames; + VstInt32 flags; + VstInt32 dumpBytes; + VstIntPtr resvd1; + char* sysexDump; + VstIntPtr resvd2; +}; + +struct VstTimeInfo { + double samplePos; + double sampleRate; + double nanoSeconds; + double ppqPos; + double tempo; + double barStartPos; + double cycleStartPos; + double cycleEndPos; + VstInt32 timeSigNumerator; + VstInt32 timeSigDenominator; + VstInt32 smpteOffset; + VstInt32 smpteFrameRate; + VstInt32 samplesToNextClock; + VstInt32 flags; +}; + +struct VstVariableIo { + float** inputs; + float** outputs; + VstInt32 numSamplesInput; + VstInt32 numSamplesOutput; + VstInt32* numSamplesInputProcessed; + VstInt32* numSamplesOutputProcessed; +}; + +inline constexpr int kVstMaxLabelLen = 64; +inline constexpr int kVstMaxShortLabelLen = 8; +inline constexpr int kVstMaxCategLabelLen = 24; +inline constexpr int kVstMaxNameLen = 64; + +struct VstParameterProperties { + float stepFloat; + float smallStepFloat; + float largeStepFloat; + char label[kVstMaxLabelLen]; + VstInt32 flags; + VstInt32 minInteger; + VstInt32 maxInteger; + VstInt32 stepInteger; + VstInt32 largeStepInteger; + char shortLabel[kVstMaxShortLabelLen]; + VstInt16 displayIndex; + VstInt16 category; + VstInt16 numParametersInCategory; + VstInt16 reserved; + char categoryLabel[kVstMaxCategLabelLen]; + char future[16]; +}; + +struct VstPinProperties { + char label[kVstMaxLabelLen]; + VstInt32 flags; + VstInt32 arrangementType; + char shortLabel[kVstMaxShortLabelLen]; + char future[48]; +}; + +struct MidiKeyName { + VstInt32 thisProgramIndex; + VstInt32 thisKeyNumber; + char keyName[kVstMaxNameLen]; + VstInt32 reserved; + VstInt32 flags; +}; + +struct VstSpeakerProperties { + float azimuth; + float elevation; + float radius; + float reserved; + char name[kVstMaxNameLen]; + VstInt32 type; + char future[28]; +}; + +struct VstSpeakerArrangement { + VstInt32 type; + VstInt32 numChannels; + VstSpeakerProperties speakers[8]; +}; + +#pragma pack(pop) + +static_assert(sizeof(AEffect) == sizeof(vst_effect_t), "VST2 effect ABI size mismatch"); +static_assert(offsetof(AEffect, dispatcher) == offsetof(vst_effect_t, control), "VST2 dispatcher ABI mismatch"); +static_assert(offsetof(AEffect, object) == offsetof(vst_effect_t, effect_internal), "VST2 object ABI mismatch"); +static_assert(offsetof(AEffect, processReplacing) == offsetof(vst_effect_t, process_float), "VST2 float process ABI mismatch"); +static_assert(offsetof(ERect, right) == offsetof(vst_rect_t, right), "VST2 editor rectangle ABI mismatch"); + +#define CCONST(a, b, c, d) VST_FOURCC(a, b, c, d) +#define kEffectMagic static_cast(VST_MAGICNUMBER) +inline constexpr VstInt32 kVstVersion = VST_VERSION_2_4_0_0; + +enum VstAEffectFlags : VstInt32 { + effFlagsHasEditor = VST_EFFECT_FLAG_EDITOR, + __effFlagsCanMonoDeprecated = 1 << 3, + effFlagsCanReplacing = VST_EFFECT_FLAG_SUPPORTS_FLOAT, + effFlagsProgramChunks = VST_EFFECT_FLAG_CHUNKS, + effFlagsIsSynth = VST_EFFECT_FLAG_INSTRUMENT, + effFlagsCanDoubleReplacing = VST_EFFECT_FLAG_SUPPORTS_DOUBLE +}; + +enum AEffectOpcodes : VstInt32 { + effOpen = 0, + effClose = 1, + effSetProgram = 2, + effGetProgram = 3, + effSetProgramName = 4, + effGetProgramName = 5, + effGetParamLabel = 6, + effGetParamDisplay = 7, + effGetParamName = 8, + effSetSampleRate = 10, + effSetBlockSize = 11, + effMainsChanged = 12, + effEditGetRect = 13, + effEditOpen = 14, + effEditClose = 15, + effEditIdle = 19, + __effIdentifyDeprecated = 22, + effGetChunk = 23, + effSetChunk = 24, + effProcessEvents = 25, + effCanBeAutomated = 26, + effString2Parameter = 27, + effGetProgramNameIndexed = 29, + effGetInputProperties = 33, + effGetOutputProperties = 34, + effGetPlugCategory = 35, + effProcessVarIo = 41, + effSetSpeakerArrangement = 42, + effSetBypass = 44, + effGetEffectName = 45, + effGetVendorString = 47, + effGetProductString = 48, + effGetVendorVersion = 49, + effVendorSpecific = 50, + effCanDo = 51, + effGetTailSize = 52, + __effIdleDeprecated = 53, + effGetParameterProperties = 56, + effGetVstVersion = 58, + effEditKeyDown = 59, + effEditKeyUp = 60, + effGetMidiProgramName = 62, + effGetCurrentMidiProgram = 63, + effGetMidiProgramCategory = 64, + effHasMidiProgramsChanged = 65, + effGetMidiKeyName = 66, + effBeginSetProgram = 67, + effEndSetProgram = 68, + effGetSpeakerArrangement = 69 +}; + +enum AudioMasterOpcodes : VstInt32 { + audioMasterAutomate = 0, + audioMasterVersion = 1, + audioMasterCurrentId = 2, + audioMasterIdle = 3, + __audioMasterWantMidiDeprecated = 6, + audioMasterGetTime = 7, + audioMasterProcessEvents = 8, + audioMasterIOChanged = 13, + audioMasterSizeWindow = 15, + audioMasterGetCurrentProcessLevel = 23, + audioMasterGetProductString = 33, + audioMasterGetVendorVersion = 34, + audioMasterUpdateDisplay = 42, + audioMasterBeginEdit = 43, + audioMasterEndEdit = 44 +}; + +enum VstPlugCategory : VstInt32 { + kPlugCategUnknown = 0, + kPlugCategEffect = VST_EFFECT_CATEGORY_EFFECT, + kPlugCategSynth = VST_EFFECT_CATEGORY_INSTRUMENT +}; + +enum VstSpeakerArrangementType : VstInt32 { + kSpeakerArrUserDefined = VST_SPEAKER_ARRANGEMENT_TYPE_CUSTOM, + kSpeakerArrEmpty = VST_SPEAKER_ARRANGEMENT_TYPE_UNKNOWN, + kSpeakerArrMono = VST_SPEAKER_ARRANGEMENT_TYPE_MONO, + kSpeakerArrStereo = VST_SPEAKER_ARRANGEMENT_TYPE_STEREO +}; + +enum VstEventTypes : VstInt32 { + kVstMidiType = VST_EVENT_TYPE_MIDI, + kVstSysExType = VST_EVENT_TYPE_MIDI_SYSEX +}; + +enum VstParameterFlags : VstInt32 { + kVstParameterIsSwitch = VST_PARAMETER_FLAG_SWITCH, + kVstParameterUsesIntegerMinMax = VST_PARAMETER_FLAG_INTEGER_LIMITS, + kVstParameterUsesFloatStep = VST_PARAMETER_FLAG_STEP_FLOAT, + kVstParameterUsesIntStep = VST_PARAMETER_FLAG_STEP_INT +}; + +enum VstPinPropertiesFlags : VstInt32 { + kVstPinIsActive = 1 << 0, + kVstPinIsStereo = VST_STREAM_FLAG_STEREO +}; + +enum VstProcessLevels : VstInt32 { + kVstProcessLevelOffline = 4 +}; + +enum VstTimeInfoFlags : VstInt32 { + kVstTransportPlaying = 1 << 1, + kVstTransportCycleActive = 1 << 2, + kVstPpqPosValid = 1 << 9, + kVstTempoValid = 1 << 10, + kVstBarsValid = 1 << 11, + kVstCyclePosValid = 1 << 12, + kVstTimeSigValid = 1 << 13 +}; + +enum VstVirtualKey : VstInt32 { + VKEY_BACK = 1, VKEY_TAB, VKEY_CLEAR, VKEY_RETURN, VKEY_PAUSE, VKEY_ESCAPE, + VKEY_SPACE, VKEY_NEXT, VKEY_END, VKEY_HOME, VKEY_LEFT, VKEY_UP, VKEY_RIGHT, + VKEY_DOWN, VKEY_PAGEUP, VKEY_PAGEDOWN, VKEY_SELECT, VKEY_PRINT, VKEY_ENTER, + VKEY_SNAPSHOT, VKEY_INSERT, VKEY_DELETE, VKEY_HELP, VKEY_NUMPAD0, VKEY_NUMPAD1, + VKEY_NUMPAD2, VKEY_NUMPAD3, VKEY_NUMPAD4, VKEY_NUMPAD5, VKEY_NUMPAD6, VKEY_NUMPAD7, + VKEY_NUMPAD8, VKEY_NUMPAD9, VKEY_MULTIPLY, VKEY_ADD, VKEY_SEPARATOR, VKEY_SUBTRACT, + VKEY_DECIMAL, VKEY_DIVIDE, VKEY_F1, VKEY_F2, VKEY_F3, VKEY_F4, VKEY_F5, VKEY_F6, + VKEY_F7, VKEY_F8, VKEY_F9, VKEY_F10, VKEY_F11, VKEY_F12, VKEY_NUMLOCK, VKEY_SCROLL, + VKEY_SHIFT, VKEY_CONTROL, VKEY_ALT, VKEY_EQUALS +}; + +enum VstModifierKey : VstInt32 { + MODIFIER_SHIFT = VST_VKEY_MODIFIER_SHIFT, + MODIFIER_ALTERNATE = VST_VKEY_MODIFIER_ALT, + MODIFIER_COMMAND = VST_VKEY_MODIFIER_SYSTEM, + MODIFIER_CONTROL = VST_VKEY_MODIFIER_CONTROL +}; diff --git a/RVCRealtimeVST/third_party/vst2sdk b/RVCRealtimeVST/third_party/vst2sdk new file mode 160000 index 0000000..339d4f3 --- /dev/null +++ b/RVCRealtimeVST/third_party/vst2sdk @@ -0,0 +1 @@ +Subproject commit 339d4f31590bf77c0d0d248e09a380ac6285e069 diff --git a/RVCRealtimeVST/third_party/vst3sdk b/RVCRealtimeVST/third_party/vst3sdk new file mode 160000 index 0000000..58f8da7 --- /dev/null +++ b/RVCRealtimeVST/third_party/vst3sdk @@ -0,0 +1 @@ +Subproject commit 58f8da7936800732561402d7936584ca4505de07 diff --git a/RVCRealtimeVST/tools/vst2_smoke.cpp b/RVCRealtimeVST/tools/vst2_smoke.cpp new file mode 100644 index 0000000..a3b1942 --- /dev/null +++ b/RVCRealtimeVST/tools/vst2_smoke.cpp @@ -0,0 +1,107 @@ +#include + +#include "aeffectx.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +VstIntPtr VSTCALLBACK hostCallback(AEffect*, VstInt32 opcode, VstInt32, VstIntPtr, void*, float) +{ + if (opcode == audioMasterVersion) + return kVstVersion; + return 0; +} + +std::wstring widen(const char* text) +{ + if (text == nullptr) + return {}; + const int size = MultiByteToWideChar(CP_UTF8, 0, text, -1, nullptr, 0); + std::wstring result(static_cast(size), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, text, -1, result.data(), size); + if (!result.empty()) + result.pop_back(); + return result; +} + +} // namespace + +int main(int argc, char** argv) +{ + if (argc != 2) { + std::cerr << "Usage: rvc-vst2-smoke \n"; + return EXIT_FAILURE; + } + + const std::wstring path = widen(argv[1]); + HMODULE module = LoadLibraryW(path.c_str()); + if (module == nullptr) { + std::cerr << "LoadLibraryW failed: " << GetLastError() << '\n'; + return EXIT_FAILURE; + } + + using PluginMain = AEffect* (VSTCALLBACK*)(audioMasterCallback); + const auto pluginMain = reinterpret_cast(GetProcAddress(module, "VSTPluginMain")); + if (pluginMain == nullptr) { + std::cerr << "VSTPluginMain export is missing\n"; + FreeLibrary(module); + return EXIT_FAILURE; + } + + AEffect* effect = pluginMain(hostCallback); + if (effect == nullptr || effect->magic != kEffectMagic || effect->dispatcher == nullptr + || effect->processReplacing == nullptr) { + std::cerr << "Invalid VST2 effect structure\n"; + FreeLibrary(module); + return EXIT_FAILURE; + } + + effect->dispatcher(effect, effOpen, 0, 0, nullptr, 0.0f); + effect->dispatcher(effect, effSetSampleRate, 0, 0, nullptr, 48000.0f); + constexpr VstInt32 frames = 512; + effect->dispatcher(effect, effSetBlockSize, 0, frames, nullptr, 0.0f); + effect->dispatcher(effect, effMainsChanged, 0, 1, nullptr, 0.0f); + + std::vector left(frames); + std::vector right(frames); + std::vector outputLeft(frames, -10.0f); + std::vector outputRight(frames, -10.0f); + for (VstInt32 i = 0; i < frames; ++i) { + left[static_cast(i)] = static_cast(0.1 * std::sin(i * 0.03)); + right[static_cast(i)] = static_cast(0.1 * std::cos(i * 0.03)); + } + float* inputs[] = {left.data(), right.data()}; + float* outputs[] = {outputLeft.data(), outputRight.data()}; + effect->processReplacing(effect, inputs, outputs, frames); + + float maxError = 0.0f; + for (VstInt32 i = 0; i < frames; ++i) { + const auto index = static_cast(i); + maxError = std::max(maxError, std::abs(outputLeft[index] - left[index])); + maxError = std::max(maxError, std::abs(outputRight[index] - right[index])); + } + + const VstInt32 inputsCount = effect->numInputs; + const VstInt32 outputsCount = effect->numOutputs; + const VstInt32 paramsCount = effect->numParams; + effect->dispatcher(effect, effMainsChanged, 0, 0, nullptr, 0.0f); + effect->dispatcher(effect, effClose, 0, 0, nullptr, 0.0f); + FreeLibrary(module); + + if (inputsCount != 2 || outputsCount != 2 || paramsCount != 12 || maxError > 1e-6f) { + std::cerr << "Unexpected VST2 behavior: inputs=" << inputsCount + << " outputs=" << outputsCount << " params=" << paramsCount + << " max_error=" << maxError << '\n'; + return EXIT_FAILURE; + } + + std::cout << "VST2 OK inputs=" << inputsCount << " outputs=" << outputsCount + << " params=" << paramsCount << " dry_max_error=" << maxError << '\n'; + return EXIT_SUCCESS; +} diff --git a/RVCRealtimeVST/tools/worker_smoke.cpp b/RVCRealtimeVST/tools/worker_smoke.cpp new file mode 100644 index 0000000..581d6da --- /dev/null +++ b/RVCRealtimeVST/tools/worker_smoke.cpp @@ -0,0 +1,94 @@ +#include "WorkerClient.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +bool waitForReady(rvc::WorkerClient& worker, const std::chrono::seconds timeout) +{ + const auto deadline = std::chrono::steady_clock::now() + timeout; + while (std::chrono::steady_clock::now() < deadline) { + if (worker.isReady()) + return true; + if (worker.status() == rvc::kStatusError) + return false; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + return false; +} + +} // namespace + +int main(const int argc, char** argv) +{ + constexpr double sampleRate = 48000.0; + constexpr double frequency = 220.0; + constexpr double pi = 3.14159265358979323846; + + rvc::WorkerClient worker; + if (argc < 4) { + std::cerr << "Usage: rvc-worker-smoke RVC_ROOT PYTHON_EXE MODEL_PTH [INDEX] [BLOCK_MS] [CROSSFADE_MS] [BLOCK_COUNT]\n"; + return EXIT_FAILURE; + } + worker.setPath(rvc::kStateRvcRoot, argv[1]); + worker.setPath(rvc::kStatePythonPath, argv[2]); + worker.setPath(rvc::kStateModelPath, argv[3]); + worker.setPath(rvc::kStateIndexPath, argc > 4 ? argv[4] : ""); + if (argc > 5) + worker.setParameter(rvc::kParamBlockMs, std::stof(argv[5])); + if (argc > 6) + worker.setParameter(rvc::kParamCrossfadeMs, std::stof(argv[6])); + const std::size_t blockCount = argc > 7 ? std::max(1, std::stoul(argv[7])) : 1; + worker.setSampleRate(sampleRate); + worker.setEnabled(true); + + if (!waitForReady(worker, std::chrono::seconds(180))) { + std::cerr << "Worker did not become ready: " << worker.statusText() << '\n'; + return EXIT_FAILURE; + } + + const std::size_t frames = worker.blockFrames(); + std::vector input(frames * blockCount); + for (std::size_t i = 0; i < input.size(); ++i) + input[i] = static_cast(0.1 * std::sin(2.0 * pi * frequency * static_cast(i) / sampleRate)); + + if (worker.pushInput(input.data(), input.size()) != input.size()) { + std::cerr << "Input ring rejected the test block\n"; + return EXIT_FAILURE; + } + const std::size_t expected = worker.latencyFrames() + input.size(); + std::vector output(expected); + std::size_t received = 0; + const auto outputDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(120); + while (received < expected && std::chrono::steady_clock::now() < outputDeadline) { + received += worker.popOutput(output.data() + received, expected - received); + if (worker.status() == rvc::kStatusError) + break; + if (received < expected) + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (received != expected) { + std::cerr << "Output ring returned " << received << " of " << expected << " frames\n"; + return EXIT_FAILURE; + } + + double sumSquares = 0.0; + for (std::size_t i = expected - frames; i < expected; ++i) + sumSquares += static_cast(output[i]) * output[i]; + const double rms = std::sqrt(sumSquares / static_cast(frames)); + + std::cout << "READY frames=" << frames + << " latency=" << worker.latencyFrames() + << " infer_ms=" << worker.inferMs() + << " output_rms=" << rms + << " drops=" << worker.droppedBlocks() + << " blocks=" << blockCount + << " status=\"" << worker.statusText() << "\"\n"; + return rms > 0.0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/RVCRealtimeVST/worker/rvc_worker.py b/RVCRealtimeVST/worker/rvc_worker.py new file mode 100644 index 0000000..122c953 --- /dev/null +++ b/RVCRealtimeVST/worker/rvc_worker.py @@ -0,0 +1,345 @@ +"""RVC inference worker for the RVC Realtime VST2/VST3 plugin.""" + +from __future__ import annotations + +import argparse +import ctypes +import json +import mmap +import os +import struct +import sys +import time +import traceback +from pathlib import Path + +MAGIC = 0x50564352 +PROTOCOL_VERSION = 1 +HEADER_BYTES = 4096 +MAX_FRAMES = 131072 +MAP_BYTES = HEADER_BYTES + MAX_FRAMES * 4 * 2 +INPUT_OFFSET = HEADER_BYTES +OUTPUT_OFFSET = HEADER_BYTES + MAX_FRAMES * 4 +STATUS_TEXT_OFFSET = 128 +STATUS_TEXT_BYTES = 512 + +STATUS_STARTING = 1 +STATUS_LOADING = 2 +STATUS_READY = 3 +STATUS_ERROR = -1 +STATUS_STOP = -2 + +SYNCHRONIZE = 0x00100000 +EVENT_MODIFY_STATE = 0x0002 +WAIT_OBJECT_0 = 0 +WAIT_TIMEOUT = 258 + + +def write_value(shared: mmap.mmap, offset: int, fmt: str, value) -> None: + struct.pack_into("<" + fmt, shared, offset, value) + + +def read_value(shared: mmap.mmap, offset: int, fmt: str): + return struct.unpack_from("<" + fmt, shared, offset)[0] + + +def write_status(shared: mmap.mmap, state: int, message: str) -> None: + write_value(shared, 8, "i", state) + encoded = message.encode("utf-8", errors="replace")[: STATUS_TEXT_BYTES - 1] + shared[STATUS_TEXT_OFFSET : STATUS_TEXT_OFFSET + STATUS_TEXT_BYTES] = b"\0" * STATUS_TEXT_BYTES + shared[STATUS_TEXT_OFFSET : STATUS_TEXT_OFFSET + len(encoded)] = encoded + shared.flush() + + +class WinEvent: + def __init__(self, name: str): + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + kernel32.OpenEventW.argtypes = [ctypes.c_uint32, ctypes.c_int, ctypes.c_wchar_p] + kernel32.OpenEventW.restype = ctypes.c_void_p + self._wait = kernel32.WaitForSingleObject + self._wait.argtypes = [ctypes.c_void_p, ctypes.c_uint32] + self._wait.restype = ctypes.c_uint32 + self._set = kernel32.SetEvent + self._set.argtypes = [ctypes.c_void_p] + self._set.restype = ctypes.c_int + self._close = kernel32.CloseHandle + self._close.argtypes = [ctypes.c_void_p] + self._close.restype = ctypes.c_int + self.handle = kernel32.OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, False, name) + if not self.handle: + raise OSError(ctypes.get_last_error(), f"OpenEventW failed: {name}") + + def wait(self, timeout_ms: int) -> int: + return int(self._wait(self.handle, timeout_ms)) + + def set(self) -> None: + if not self._set(self.handle): + raise OSError(ctypes.get_last_error(), "SetEvent failed") + + def close(self) -> None: + if self.handle: + self._close(self.handle) + self.handle = None + + +class RVCStreamEngine: + def __init__(self, cfg: dict): + self.root = Path(cfg["rvc_root"]).resolve() + os.chdir(self.root) + sys.path.insert(0, str(self.root)) + os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") + os.environ.setdefault("OMP_NUM_THREADS", "4") + + import librosa + import numpy as np + import torch + import torch.nn.functional as F + import torchaudio.transforms as tat + + from configs.config import Config + from infer import rtrvc + from tools.cuda_graph import run_cuda_graph + + self.librosa = librosa + self.np = np + self.torch = torch + self.F = F + self.tat = tat + self.run_cuda_graph = run_cuda_graph + + self.sample_rate = int(cfg["sample_rate"]) + self.block_ms = float(cfg["block_ms"]) + self.crossfade_ms = float(cfg["crossfade_ms"]) + self.extra_ms = float(cfg["extra_ms"]) + self.zc = max(1, self.sample_rate // 100) + self.block_frame = int(round(self.block_ms / 1000 * self.sample_rate / self.zc) * self.zc) + self.block_frame_16k = 160 * self.block_frame // self.zc + self.crossfade_frame = int(round(self.crossfade_ms / 1000 * self.sample_rate / self.zc) * self.zc) + # Match the source realtime GUI: SOLA overlap is capped at 40 ms and + # remains independent of the audio callback block length. + self.sola_buffer_frame = min(self.crossfade_frame, 4 * self.zc) + self.effective_crossfade_ms = 1000.0 * self.sola_buffer_frame / self.sample_rate + self.sola_search_frame = self.zc + self.extra_frame = int(round(self.extra_ms / 1000 * self.sample_rate / self.zc) * self.zc) + + self.config = Config() + self.rvc = rtrvc.RVC( + 0.0, + 0.0, + str(Path(cfg["model_path"]).resolve()), + str(Path(cfg["index_path"]).resolve()) if cfg.get("index_path") else "", + 0.0, + self.config, + None, + ) + + total_frames = self.extra_frame + self.crossfade_frame + self.sola_search_frame + self.block_frame + self.input_wav = torch.zeros(total_frames, device=self.config.device, dtype=torch.float32) + self.input_wav_res = torch.zeros(160 * total_frames // self.zc, device=self.config.device, dtype=torch.float32) + self.rms_buffer = np.zeros(4 * self.zc, dtype="float32") + self.sola_buffer = torch.zeros(self.sola_buffer_frame, device=self.config.device, dtype=torch.float32) + self.sola_den_kernel = torch.ones(1, 1, self.sola_buffer_frame, device=self.config.device, dtype=torch.float32) + self.skip_head = self.extra_frame // self.zc + self.return_length = (self.block_frame + self.sola_buffer_frame + self.sola_search_frame) // self.zc + self.fade_in_window = torch.sin( + 0.5 * np.pi * torch.linspace(0.0, 1.0, steps=self.sola_buffer_frame, device=self.config.device) + ) ** 2 + self.fade_out_window = 1 - self.fade_in_window + self.resampler = tat.Resample(orig_freq=self.sample_rate, new_freq=16000, dtype=torch.float32).to(self.config.device) + self.resampler2 = None + if self.rvc.tgt_sr != self.sample_rate: + self.resampler2 = tat.Resample(orig_freq=self.rvc.tgt_sr, new_freq=self.sample_rate, dtype=torch.float32).to(self.config.device) + self.last_pitch = None + self.last_formant = None + self.last_index_rate = None + + def prewarm(self) -> None: + phase = self.torch.arange(self.block_frame, device=self.config.device, dtype=self.torch.float32) + probe = 0.05 * self.torch.sin(2 * self.np.pi * 220.0 * phase / self.sample_rate) + self.process(probe.cpu().numpy(), 12.0, 0.0, 0.0, 0.5, -60.0, 0) + if self.torch.device(self.config.device).type == "cuda": + self.torch.cuda.synchronize(self.config.device) + + self.input_wav.zero_() + self.input_wav_res.zero_() + self.rms_buffer.fill(0.0) + self.sola_buffer.zero_() + if hasattr(self.rvc, "cache_pitch"): + self.rvc.cache_pitch.zero_() + if hasattr(self.rvc, "cache_pitchf"): + self.rvc.cache_pitchf.zero_() + + def process(self, audio, pitch: float, formant: float, index_rate: float, + rms_mix: float, threshold: float, f0_method: int): + np = self.np + torch = self.torch + F = self.F + if len(audio) != self.block_frame: + raise ValueError(f"block mismatch: worker={self.block_frame}, request={len(audio)}") + + if self.last_pitch != pitch: + self.rvc.change_key(pitch) + self.last_pitch = pitch + if self.last_formant != formant: + self.rvc.change_formant(formant) + self.last_formant = formant + if self.last_index_rate != index_rate: + self.rvc.change_index_rate(index_rate) + self.last_index_rate = index_rate + + indata = np.asarray(audio, dtype=np.float32).copy() + if threshold > -60.0: + gated = np.append(self.rms_buffer, indata) + rms = self.librosa.feature.rms(y=gated, frame_length=4 * self.zc, hop_length=self.zc)[:, 2:] + self.rms_buffer[:] = gated[-4 * self.zc :] + gated = gated[2 * self.zc - self.zc // 2 :] + below = self.librosa.amplitude_to_db(rms, ref=1.0)[0] < threshold + for i, mute in enumerate(below): + if mute: + gated[i * self.zc : (i + 1) * self.zc] = 0 + indata = gated[self.zc // 2 :] + + self.input_wav[:-self.block_frame] = self.input_wav[self.block_frame:].clone() + self.input_wav[-self.block_frame:] = torch.from_numpy(indata).to(self.config.device) + self.input_wav_res[:-self.block_frame_16k] = self.input_wav_res[self.block_frame_16k:].clone() + resample_input = self.input_wav[-self.block_frame - 2 * self.zc :] + resampled = self.run_cuda_graph( + self.resampler, + "vst-input-resample", + lambda value: self.resampler(value), + resample_input, + )[160:] + self.input_wav_res[-self.block_frame_16k:] = resampled[-self.block_frame_16k:] + + method = ("rmvpe", "fcpe", "pm")[max(0, min(2, int(f0_method)))] + infer_wav = self.rvc.infer( + self.input_wav_res, + self.block_frame_16k, + self.skip_head, + self.return_length, + method, + ) + if self.resampler2 is not None: + infer_wav = self.run_cuda_graph( + self.resampler2, + "vst-output-resample", + lambda value: self.resampler2(value), + infer_wav, + ) + + if rms_mix < 1.0: + input_tail = self.input_wav[self.extra_frame :] + rms1 = self.librosa.feature.rms( + y=input_tail[: infer_wav.shape[0]].cpu().numpy(), frame_length=4 * self.zc, hop_length=self.zc + ) + rms1 = torch.from_numpy(rms1).to(self.config.device) + rms1 = F.interpolate(rms1.unsqueeze(0), size=infer_wav.shape[0] + 1, mode="linear", align_corners=True)[0, 0, :-1] + rms2 = self.librosa.feature.rms( + y=infer_wav.cpu().numpy(), frame_length=4 * self.zc, hop_length=self.zc + ) + rms2 = torch.from_numpy(rms2).to(self.config.device) + rms2 = F.interpolate(rms2.unsqueeze(0), size=infer_wav.shape[0] + 1, mode="linear", align_corners=True)[0, 0, :-1] + rms2 = torch.maximum(rms2, torch.full_like(rms2, 1e-3)) + infer_wav *= torch.pow(rms1 / rms2, 1.0 - rms_mix) + + conv_input = infer_wav[None, None, : self.sola_buffer_frame + self.sola_search_frame] + cor_nom = F.conv1d(conv_input, self.sola_buffer[None, None, :]) + cor_den = torch.sqrt(F.conv1d(conv_input**2, self.sola_den_kernel) + 1e-8) + sola_offset = int(torch.argmax(cor_nom[0, 0] / cor_den[0, 0])) + infer_wav = infer_wav[sola_offset:] + infer_wav[: self.sola_buffer_frame] *= self.fade_in_window + infer_wav[: self.sola_buffer_frame] += self.sola_buffer * self.fade_out_window + self.sola_buffer[:] = infer_wav[self.block_frame : self.block_frame + self.sola_buffer_frame] + return infer_wav[: self.block_frame].float().cpu().numpy() + + +def run(args: argparse.Namespace) -> int: + shared = None + request_event = None + response_event = None + try: + shared = mmap.mmap(-1, MAP_BYTES, tagname=args.map, access=mmap.ACCESS_WRITE) + request_event = WinEvent(args.request) + response_event = WinEvent(args.response) + if read_value(shared, 0, "I") != MAGIC or read_value(shared, 4, "I") != PROTOCOL_VERSION: + raise RuntimeError("RVC VST protocol mismatch") + write_status(shared, STATUS_LOADING, "Loading Python and CUDA") + with open(args.config, "r", encoding="utf-8") as handle: + cfg = json.load(handle) + # RVC's Config parses process-wide CLI flags intended for its WebUI. + sys.argv = [sys.argv[0]] + engine = RVCStreamEngine(cfg) + write_status(shared, STATUS_LOADING, "Prewarming CUDA and F0") + engine.prewarm() + write_status(shared, STATUS_READY, f"Ready (actual CF {engine.effective_crossfade_ms:.0f} ms)") + + import numpy as np + + input_view = np.ndarray((MAX_FRAMES,), dtype=np.float32, buffer=shared, offset=INPUT_OFFSET) + output_view = np.ndarray((MAX_FRAMES,), dtype=np.float32, buffer=shared, offset=OUTPUT_OFFSET) + last_sequence = 0 + while True: + result = request_event.wait(1000) + if result == WAIT_TIMEOUT: + continue + if result != WAIT_OBJECT_0: + raise OSError("WaitForSingleObject failed") + if read_value(shared, 8, "i") == STATUS_STOP: + break + sequence = read_value(shared, 12, "I") + if sequence == last_sequence: + continue + frames = read_value(shared, 20, "I") + if frames <= 0 or frames > MAX_FRAMES: + raise ValueError(f"invalid frame count: {frames}") + started = time.perf_counter() + processed = engine.process( + input_view[:frames], + read_value(shared, 32, "f"), + read_value(shared, 36, "f"), + read_value(shared, 40, "f"), + read_value(shared, 44, "f"), + read_value(shared, 48, "f"), + read_value(shared, 64, "I"), + ) + output_view[:frames] = processed + write_value(shared, 56, "f", (time.perf_counter() - started) * 1000.0) + write_value(shared, 16, "I", sequence) + write_value(shared, 8, "i", STATUS_READY) + last_sequence = sequence + response_event.set() + return 0 + except BaseException: + message = traceback.format_exc() + try: + Path(args.config + ".log").write_text(message, encoding="utf-8") + except Exception: + pass + try: + if shared is not None: + write_status(shared, STATUS_ERROR, message[-500:]) + if response_event is not None: + response_event.set() + except Exception: + pass + return 1 + finally: + if request_event is not None: + request_event.close() + if response_event is not None: + response_event.close() + if shared is not None: + shared.close() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--map", required=True) + parser.add_argument("--request", required=True) + parser.add_argument("--response", required=True) + parser.add_argument("--config", required=True) + return parser.parse_args() + + +if __name__ == "__main__": + raise SystemExit(run(parse_args()))