Add RVC Realtime VST2/VST3 source project

This commit is contained in:
RVC-Boss
2026-08-03 22:16:30 +08:00
parent 4338f12c3c
commit d77bfce2c3
28 changed files with 3314 additions and 0 deletions

9
.gitmodules vendored Normal file
View File

@@ -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

17
RVCRealtimeVST/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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
"$<TARGET_FILE_DIR:rvc-worker-smoke>/RVCRealtime.resources/worker"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_CURRENT_SOURCE_DIR}/worker/rvc_worker.py"
"$<TARGET_FILE_DIR:rvc-worker-smoke>/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()

View File

@@ -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.

View File

@@ -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.

41
RVCRealtimeVST/config.h Normal file
View File

@@ -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 ""

View File

@@ -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 missingRVCRealtime.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。

Binary file not shown.

View File

@@ -0,0 +1,37 @@
#include "resource.h"
#include "../config.h"
#include <winres.h>
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

View File

@@ -0,0 +1,3 @@
#pragma once
#define IDR_RVC_FONT 101

View File

@@ -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"

View File

@@ -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."

View File

@@ -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."
}

View File

@@ -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"
}

View File

@@ -0,0 +1,663 @@
#include "RVCRealtime.h"
#include "IPlug_include_in_plug_src.h"
#include "IPlugPaths.h"
#include "IControls.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#if defined(_WIN32)
#include <windows.h>
#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<wchar_t> value(32768, L'\0');
GetPrivateProfileStringW(L"Paths", key, L"", value.data(), static_cast<DWORD>(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<rvc::ParameterId>(paramIdx), static_cast<float>(value));
if (paramIdx == kEngine) {
if (value >= 0.5) {
std::string error;
if (!ValidateConfiguration(error)) {
{
std::lock_guard<std::mutex> lock(mStateMutex);
mValidationMessage.Set(error.c_str());
}
GetParam(kEngine)->Set(0.0);
SendParameterValueFromAPI(kEngine, 0.0, false);
mWorker.setEnabled(false);
return;
}
{
std::lock_guard<std::mutex> 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<size_t>(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<size_t>(nFrames) * sizeof(sample));
}
};
if (nFrames > static_cast<int>(mMonoInput.size())) {
passthrough();
return;
}
const bool requested = GetParam(kEngine)->Bool();
if (!requested || !mWorker.isReady()) {
mActiveBlend = std::max(0.0f, mActiveBlend - static_cast<float>(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<size_t>(frame)] = static_cast<float>(0.5 * (inL[frame] + inR[frame]));
mWorker.pushInput(mMonoInput.data(), static_cast<size_t>(nFrames));
const size_t wetRead = mWorker.popOutput(mWetOutput.data(), static_cast<size_t>(nFrames));
std::fill(mWetOutput.begin() + static_cast<ptrdiff_t>(wetRead),
mWetOutput.begin() + nFrames, 0.0f);
const float mix = static_cast<float>(GetParam(kDryWet)->Value() / 100.0);
const float gain = static_cast<float>(std::pow(10.0, GetParam(kOutputGain)->Value() / 20.0));
const int maxDelay = static_cast<int>(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<float>(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<size_t>(write) * 2] = static_cast<float>(inL[frame]);
mDryDelay[static_cast<size_t>(write) * 2 + 1] = static_cast<float>(inR[frame]);
const float wet = mWetOutput[static_cast<size_t>(frame)] * gain;
const float processedL = mDryDelay[static_cast<size_t>(read) * 2] * (1.0f - mix) + wet * mix;
const float processedR = mDryDelay[static_cast<size_t>(read) * 2 + 1] * (1.0f - mix) + wet * mix;
outL[frame] = static_cast<sample>(inL[frame] * (1.0f - mActiveBlend) + processedL * mActiveBlend);
if (outR)
outR[frame] = static_cast<sample>(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<size_t>(nFrames) * sizeof(sample));
}
#endif
void RVCRealtime::OnIdle()
{
#if IPLUG_EDITOR
if (GetUI() == nullptr)
return;
std::string validationMessage;
{
std::lock_guard<std::mutex> lock(mStateMutex);
validationMessage = mValidationMessage.Get();
}
if (auto* status = GetUI()->GetControlWithTag(kCtrlStatus))
status->As<ITextControl>()->SetStr(validationMessage.empty() ? StatusName(mWorker.status()) : "CONFIG ERROR");
if (auto* detail = GetUI()->GetControlWithTag(kCtrlStatusDetail))
detail->As<ITextControl>()->SetStr(validationMessage.empty() ? mWorker.statusText().c_str() : validationMessage.c_str());
if (auto* performance = GetUI()->GetControlWithTag(kCtrlPerformance))
performance->As<ITextControl>()->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<std::mutex> 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<std::mutex> 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<int>(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<size_t>(std::max(blockSize, 131072));
mMonoInput.assign(scratch, 0.0f);
mWetOutput.assign(scratch, 0.0f);
mDryDelay.assign(static_cast<size_t>(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<std::mutex> 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<std::mutex> lock(mStateMutex);
mPythonPath.Set(value);
mValidationMessage.Set("");
}
mWorker.setPath(rvc::kStatePythonPath, value);
StopEngineForPathChange();
UpdateFileLabels();
}
void RVCRealtime::SetModelPath(const char* path)
{
{
std::lock_guard<std::mutex> lock(mStateMutex);
mModelPath.Set(path);
}
mWorker.setPath(rvc::kStateModelPath, path);
StopEngineForPathChange();
UpdateFileLabels();
}
void RVCRealtime::SetIndexPath(const char* path)
{
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mStateMutex);
if (auto* root = GetUI()->GetControlWithTag(kCtrlRvcRoot))
root->As<ITextControl>()->SetStr(mRvcRoot.Get());
if (auto* python = GetUI()->GetControlWithTag(kCtrlPythonPath))
python->As<ITextControl>()->SetStr(mPythonPath.Get());
if (auto* model = GetUI()->GetControlWithTag(kCtrlModelName))
model->As<ITextControl>()->SetStr(mModelPath.get_filepart());
if (auto* index = GetUI()->GetControlWithTag(kCtrlIndexName))
index->As<ITextControl>()->SetStr(mIndexPath.get_filepart());
#endif
}

View File

@@ -0,0 +1,96 @@
#pragma once
#include "IPlug_include_in_plug_hdr.h"
#include "RvcParameters.hpp"
#include "WorkerClient.hpp"
#include <atomic>
#include <mutex>
#include <string>
#include <vector>
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<float> mMonoInput;
std::vector<float> mWetOutput;
std::vector<float> mDryDelay;
int mDryWritePosition = 0;
std::atomic<int> 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;
};

View File

@@ -0,0 +1,68 @@
#pragma once
#include <cstdint>
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

View File

@@ -0,0 +1,98 @@
#pragma once
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstring>
#include <memory>
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<std::size_t>(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<float[]> data_;
std::size_t capacity_ = 0;
std::size_t mask_ = 0;
alignas(64) std::atomic<std::size_t> write_ {0};
alignas(64) std::atomic<std::size_t> read_ {0};
};
} // namespace rvc

View File

@@ -0,0 +1,587 @@
#include "WorkerClient.hpp"
#include "config.h"
#if !defined(_WIN32)
#error RVC Realtime worker bridge currently targets Windows.
#endif
#include <windows.h>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstring>
#include <iomanip>
#include <sstream>
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 <typename T>
void writeAt(void* const base, const std::size_t offset, const T value)
{
std::memcpy(static_cast<unsigned char*>(base) + offset, &value, sizeof(T));
}
template <typename T>
T readAt(const void* const base, const std::size_t offset)
{
T value {};
std::memcpy(&value, static_cast<const unsigned char*>(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<std::size_t>(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<std::size_t>(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<LPCWSTR>(&moduleAnchor), &module))
return {};
std::vector<wchar_t> path(32768, L'\0');
const DWORD length = GetModuleFileNameW(module, path.data(), static_cast<DWORD>(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<wchar_t> temporaryPath(32768, L'\0');
const DWORD length = GetTempPathW(static_cast<DWORD>(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<int>(ch);
else
out << static_cast<char>(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<std::mutex> 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<std::mutex> lock(statusTextMutex_);
return statusText_;
}
WorkerClient::Paths WorkerClient::pathsSnapshot() const
{
std::lock_guard<std::mutex> lock(pathsMutex_);
return paths_;
}
void WorkerClient::setStatus(const int status, const std::string& text)
{
status_.store(status, std::memory_order_release);
std::lock_guard<std::mutex> 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<uint32_t>(std::round(seconds * sampleRate / zc) * zc);
return std::clamp<uint32_t>(frames, static_cast<uint32_t>(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<uint32_t>(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<DWORD>(contents.size()), &bytesWritten, nullptr);
const DWORD writeError = written != FALSE ? ERROR_SUCCESS : GetLastError();
CloseHandle(file);
if (written == FALSE || bytesWritten != static_cast<DWORD>(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<Ipc>();
const DWORD pid = GetCurrentProcessId();
static std::atomic<uint32_t> 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<uint32_t>(ipc_->view, 0, kMagic);
writeAt<uint32_t>(ipc_->view, 4, kProtocolVersion);
writeAt<int32_t>(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<wchar_t> 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<int32_t>(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<uint32_t>(ipc_->view, 12, sequence);
writeAt<uint32_t>(ipc_->view, 20, frames);
writeAt<uint32_t>(ipc_->view, 24, static_cast<uint32_t>(sampleRate_.load(std::memory_order_relaxed)));
writeAt<float>(ipc_->view, 32, parameters_[kParamPitch].load(std::memory_order_relaxed));
writeAt<float>(ipc_->view, 36, parameters_[kParamFormant].load(std::memory_order_relaxed));
writeAt<float>(ipc_->view, 40, parameters_[kParamIndexRate].load(std::memory_order_relaxed));
writeAt<float>(ipc_->view, 44, parameters_[kParamRmsMix].load(std::memory_order_relaxed));
writeAt<float>(ipc_->view, 48, parameters_[kParamThreshold].load(std::memory_order_relaxed));
writeAt<uint32_t>(ipc_->view, 64, static_cast<uint32_t>(std::round(parameters_[kParamF0Method].load(std::memory_order_relaxed))));
std::memcpy(static_cast<unsigned char*>(ipc_->view) + kInputOffset, requestBuffer_.data(), frames * sizeof(float));
SetEvent(ipc_->requestEvent);
const DWORD timeout = std::max<DWORD>(5000, static_cast<DWORD>(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<uint32_t>(ipc_->view, 16) != sequence) {
droppedBlocks_.fetch_add(1, std::memory_order_relaxed);
return true;
}
const int workerState = readAt<int32_t>(ipc_->view, 8);
if (workerState < 0) {
const char* const text = reinterpret_cast<const char*>(static_cast<unsigned char*>(ipc_->view) + kStatusTextOffset);
setStatus(kStatusError, text[0] != '\0' ? text : "Worker error");
return false;
}
inferMs_.store(readAt<float>(ipc_->view, 56), std::memory_order_relaxed);
std::memcpy(responseBuffer_.data(), static_cast<unsigned char*>(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<int32_t>(ipc_->view, 8);
const char* const workerText = reinterpret_cast<const char*>(static_cast<unsigned char*>(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

View File

@@ -0,0 +1,87 @@
#pragma once
#include "RvcParameters.hpp"
#include "SpscRing.hpp"
#include <array>
#include <atomic>
#include <cstdint>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
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<float>(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<std::atomic<float>, kParameterCount> parameters_ {};
std::atomic<bool> enabled_ {false};
std::atomic<bool> stopRequested_ {false};
std::atomic<bool> ready_ {false};
std::atomic<int> status_ {kStatusOff};
std::atomic<float> inferMs_ {0.0f};
std::atomic<uint32_t> droppedBlocks_ {0};
std::atomic<uint32_t> blockFrames_ {6240};
std::atomic<uint32_t> latencyFrames_ {12480};
std::atomic<double> sampleRate_ {48000.0};
std::atomic<uint64_t> configVersion_ {1};
mutable std::mutex pathsMutex_;
Paths paths_;
mutable std::mutex statusTextMutex_;
std::string statusText_ {"Off"};
std::unique_ptr<Ipc> ipc_;
std::vector<float> requestBuffer_;
std::vector<float> responseBuffer_;
std::thread thread_;
};
} // namespace rvc

View File

@@ -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.

View File

@@ -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 <cstddef>
#include <cstdint>
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<VstInt32>(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
};

View File

@@ -0,0 +1,107 @@
#include <windows.h>
#include "aeffectx.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
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<std::size_t>(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 <plugin.dll>\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<PluginMain>(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<float> left(frames);
std::vector<float> right(frames);
std::vector<float> outputLeft(frames, -10.0f);
std::vector<float> outputRight(frames, -10.0f);
for (VstInt32 i = 0; i < frames; ++i) {
left[static_cast<std::size_t>(i)] = static_cast<float>(0.1 * std::sin(i * 0.03));
right[static_cast<std::size_t>(i)] = static_cast<float>(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<std::size_t>(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;
}

View File

@@ -0,0 +1,94 @@
#include "WorkerClient.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <thread>
#include <vector>
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<std::size_t>(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<float> input(frames * blockCount);
for (std::size_t i = 0; i < input.size(); ++i)
input[i] = static_cast<float>(0.1 * std::sin(2.0 * pi * frequency * static_cast<double>(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<float> 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<double>(output[i]) * output[i];
const double rms = std::sqrt(sumSquares / static_cast<double>(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;
}

View File

@@ -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()))