mirror of
https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI.git
synced 2026-08-29 01:59:23 +02:00
Add RVC Realtime VST2/VST3 source project
This commit is contained in:
9
.gitmodules
vendored
Normal file
9
.gitmodules
vendored
Normal 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
17
RVCRealtimeVST/.gitignore
vendored
Normal 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
|
||||||
95
RVCRealtimeVST/CMakeLists.txt
Normal file
95
RVCRealtimeVST/CMakeLists.txt
Normal 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()
|
||||||
13
RVCRealtimeVST/LICENSE.txt
Normal file
13
RVCRealtimeVST/LICENSE.txt
Normal 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.
|
||||||
40
RVCRealtimeVST/THIRD_PARTY_NOTICES.md
Normal file
40
RVCRealtimeVST/THIRD_PARTY_NOTICES.md
Normal 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
41
RVCRealtimeVST/config.h
Normal 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 ""
|
||||||
87
RVCRealtimeVST/resources/README.txt
Normal file
87
RVCRealtimeVST/resources/README.txt
Normal 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 missing:RVCRealtime.resources 没有与 DLL 放在同一目录。
|
||||||
|
- 启动提示 Python 错误:重新选择 RVC ROOT,并检查 runtime\python.exe 或手动选择 Python。
|
||||||
|
- 启动提示模型或源码缺失:检查界面选择路径以及 RVC 整合包内容。
|
||||||
|
- 系统提示缺少 MSVC DLL:安装 Microsoft Visual C++ 2015-2022 Redistributable x64。
|
||||||
|
|
||||||
|
操作说明
|
||||||
|
--------
|
||||||
|
- 单击并拖动滑块:调整参数。
|
||||||
|
- 双击滑块数值:直接输入数字。
|
||||||
|
- BLOCK 可选范围为 20~1000 ms。
|
||||||
|
- CROSSFADE 可选范围为 10~100 ms。
|
||||||
|
- 与原版实时 GUI 一致,实际 SOLA Crossfade 为 CROSSFADE 和 40 ms 中的较小值,与 BLOCK 独立。
|
||||||
|
- 例如 BLOCK 20 ms、CROSSFADE 100 ms 时,实际 SOLA Crossfade 为 40 ms。
|
||||||
|
- 状态栏的 actual CF 会显示当前实际使用的 Crossfade。
|
||||||
BIN
RVCRealtimeVST/resources/fonts/Roboto-Regular.ttf
Normal file
BIN
RVCRealtimeVST/resources/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
37
RVCRealtimeVST/resources/main.rc
Normal file
37
RVCRealtimeVST/resources/main.rc
Normal 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
|
||||||
3
RVCRealtimeVST/resources/resource.h
Normal file
3
RVCRealtimeVST/resources/resource.h
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#define IDR_RVC_FONT 101
|
||||||
76
RVCRealtimeVST/scripts/build.ps1
Normal file
76
RVCRealtimeVST/scripts/build.ps1
Normal 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"
|
||||||
76
RVCRealtimeVST/scripts/prepare-dependencies.ps1
Normal file
76
RVCRealtimeVST/scripts/prepare-dependencies.ps1
Normal 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."
|
||||||
91
RVCRealtimeVST/scripts/test-all.ps1
Normal file
91
RVCRealtimeVST/scripts/test-all.ps1
Normal 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."
|
||||||
|
}
|
||||||
44
RVCRealtimeVST/scripts/test-worker.ps1
Normal file
44
RVCRealtimeVST/scripts/test-worker.ps1
Normal 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"
|
||||||
|
}
|
||||||
663
RVCRealtimeVST/src/RVCRealtime.cpp
Normal file
663
RVCRealtimeVST/src/RVCRealtime.cpp
Normal 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
|
||||||
|
}
|
||||||
96
RVCRealtimeVST/src/RVCRealtime.h
Normal file
96
RVCRealtimeVST/src/RVCRealtime.h
Normal 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;
|
||||||
|
};
|
||||||
68
RVCRealtimeVST/src/RvcParameters.hpp
Normal file
68
RVCRealtimeVST/src/RvcParameters.hpp
Normal 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
|
||||||
98
RVCRealtimeVST/src/SpscRing.hpp
Normal file
98
RVCRealtimeVST/src/SpscRing.hpp
Normal 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
|
||||||
587
RVCRealtimeVST/src/WorkerClient.cpp
Normal file
587
RVCRealtimeVST/src/WorkerClient.cpp
Normal 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
|
||||||
87
RVCRealtimeVST/src/WorkerClient.hpp
Normal file
87
RVCRealtimeVST/src/WorkerClient.hpp
Normal 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
|
||||||
1
RVCRealtimeVST/third_party/iPlug2
vendored
Submodule
1
RVCRealtimeVST/third_party/iPlug2
vendored
Submodule
Submodule RVCRealtimeVST/third_party/iPlug2 added at 5c2df9dce3
201
RVCRealtimeVST/third_party/licenses/Roboto-Apache-2.0.txt
vendored
Normal file
201
RVCRealtimeVST/third_party/licenses/Roboto-Apache-2.0.txt
vendored
Normal 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.
|
||||||
336
RVCRealtimeVST/third_party/vst2-compat/aeffectx.h
vendored
Normal file
336
RVCRealtimeVST/third_party/vst2-compat/aeffectx.h
vendored
Normal 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
|
||||||
|
};
|
||||||
1
RVCRealtimeVST/third_party/vst2sdk
vendored
Submodule
1
RVCRealtimeVST/third_party/vst2sdk
vendored
Submodule
Submodule RVCRealtimeVST/third_party/vst2sdk added at 339d4f3159
1
RVCRealtimeVST/third_party/vst3sdk
vendored
Submodule
1
RVCRealtimeVST/third_party/vst3sdk
vendored
Submodule
Submodule RVCRealtimeVST/third_party/vst3sdk added at 58f8da7936
107
RVCRealtimeVST/tools/vst2_smoke.cpp
Normal file
107
RVCRealtimeVST/tools/vst2_smoke.cpp
Normal 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;
|
||||||
|
}
|
||||||
94
RVCRealtimeVST/tools/worker_smoke.cpp
Normal file
94
RVCRealtimeVST/tools/worker_smoke.cpp
Normal 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;
|
||||||
|
}
|
||||||
345
RVCRealtimeVST/worker/rvc_worker.py
Normal file
345
RVCRealtimeVST/worker/rvc_worker.py
Normal 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()))
|
||||||
Reference in New Issue
Block a user