diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-06-22 05:21:19 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-06-22 05:21:19 +0800 |
| commit | 51c46d43f5c1be332566305b8f8b40ac9041a06a (patch) | |
| tree | d903b119f2cde121d0e47c5941f07567e44b1c27 | |
| parent | ff5e4faa8082bcaad508021b765e1192462ba5ea (diff) | |
feat(unreal): add DMVOP TCP client plugin for Unreal Engine
| -rw-r--r-- | README.md | 17 | ||||
| -rw-r--r-- | binding/unreal/.gitignore | 6 | ||||
| -rw-r--r-- | binding/unreal/DMVOPBridge/DMVOPBridge.uplugin | 18 | ||||
| -rw-r--r-- | binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs | 13 | ||||
| -rw-r--r-- | binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.cpp | 2 | ||||
| -rw-r--r-- | binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.h | 9 | ||||
| -rw-r--r-- | binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp | 93 | ||||
| -rw-r--r-- | binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h | 39 | ||||
| -rw-r--r-- | binding/unreal/build-plugin.ps1 | 72 |
9 files changed, 269 insertions, 0 deletions
@@ -36,6 +36,23 @@ dmvop --list-devices # List microphone devices dmvop --download-model=small # Download whisper model ``` +## Game Engine Binding + +### 1. Unity + +Place `binding\unity\DMVOPListener.cs` into your Unity project's `Assets` folder and it's ready to use. + +### 2. Unreal + +1. Build the binding plugin with the following command: + +```powershell +.\binding\unreal\build-plugin.ps1 -UE "F:\YourUnrealEngine\UE_5.x" +``` + +2. Copy the `binding\unreal\build\DMVOPBridge` directory to your project or engine's `Plugins` folder. +3. Enable the `DMVOPBridge` plugin in the Unreal Editor. + ## License Under WTFPL diff --git a/binding/unreal/.gitignore b/binding/unreal/.gitignore new file mode 100644 index 0000000..03d866a --- /dev/null +++ b/binding/unreal/.gitignore @@ -0,0 +1,6 @@ +build/ +.host/ + +DMVOPBridge/Config +DMVOPBridge/Binaries +DMVOPBridge/Intermediate diff --git a/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin b/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin new file mode 100644 index 0000000..349425a --- /dev/null +++ b/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin @@ -0,0 +1,18 @@ +{ + "FileVersion": 3, + "Version": 0, + "VersionName": "0.1.0", + "FriendlyName": "Dumb Voice Protocol - UE Binding", + "Description": "TCP client for dmvop voice input in Unreal Engine", + "Category": "Input", + "CreatedBy": "Weicao-CatilGrass", + "CreatedByURL": "https://github.com/catilgrass/dumb-voice-protocol", + "DocsURL": "https://github.com/catilgrass/dumb-voice-protocol", + "Modules": [ + { + "Name": "DMVOPBridge", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ] +} diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs new file mode 100644 index 0000000..5bc7d15 --- /dev/null +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs @@ -0,0 +1,13 @@ +using UnrealBuildTool; + +public class DMVOPBridge : ModuleRules +{ + public DMVOPBridge(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + PublicDependencyModuleNames.AddRange(new string[] { + "Core", "CoreUObject", "Engine", + "Sockets", "Networking" + }); + } +} diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.cpp b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.cpp new file mode 100644 index 0000000..2dc624c --- /dev/null +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.cpp @@ -0,0 +1,2 @@ +#include "DMVOPBridge.h" +IMPLEMENT_MODULE(FDMVOPBridgeModule, DMVOPBridge) diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.h b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.h new file mode 100644 index 0000000..184c775 --- /dev/null +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.h @@ -0,0 +1,9 @@ +#pragma once +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +class FDMVOPBridgeModule : public IModuleInterface { +public: + virtual void StartupModule() override {} + virtual void ShutdownModule() override {} +}; diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp new file mode 100644 index 0000000..211b4ba --- /dev/null +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp @@ -0,0 +1,93 @@ +#include "DMVOPBridgeClient.h" +#include "Common/TcpSocketBuilder.h" +#include "IPAddress.h" + +UDMVOPClient::UDMVOPClient() : Socket(nullptr) {} + +UDMVOPClient::~UDMVOPClient() { + bRunning.store(false); + // Don't call Disconnect() here — UE GC may crash. + // Socket will be cleaned up by the OS on process exit. +} + +void UDMVOPClient::Connect(const FString &Host, int32 Port) { + if (Socket) + return; + + FIPv4Address IP; + if (!FIPv4Address::Parse(Host, IP)) { + UE_LOG(LogTemp, Error, TEXT("DMVOP: Invalid IP %s"), *Host); + return; + } + + TSharedRef<FInternetAddr> Addr = + ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); + Addr->SetIp(IP.Value); + Addr->SetPort(Port); + + Socket = FTcpSocketBuilder(TEXT("DMVOPSocket")).AsNonBlocking().Build(); + if (!Socket->Connect(*Addr)) { + UE_LOG(LogTemp, Error, TEXT("DMVOP: Failed to connect")); + Socket->Close(); + ISocketSubsystem::Get()->DestroySocket(Socket); + Socket = nullptr; + return; + } + + UE_LOG(LogTemp, Log, TEXT("DMVOP: Connected to %s:%d"), *Host, Port); + + // Start reader thread + bRunning.store(true); + TWeakObjectPtr<UDMVOPClient> WeakThis(this); + FSocket *Sock = Socket; + + ReaderThread = std::thread([WeakThis, Sock]() { + TArray<uint8> Buf; + Buf.SetNumUninitialized(4096); + FString Partial; + + while (true) { + int32 Read = 0; + if (!Sock->Recv(Buf.GetData(), Buf.Num(), Read, + ESocketReceiveFlags::None) || + Read == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + Partial += FString( + Read, UTF8_TO_TCHAR(reinterpret_cast<const char *>(Buf.GetData()))); + + int32 Idx; + while (Partial.FindChar('\n', Idx)) { + FString Line = Partial.Left(Idx).TrimEnd(); + Partial = Partial.Mid(Idx + 1); + if (Line.IsEmpty()) + continue; + + float Vol = 0.0f; + FString Text = Line; + int32 Comma; + if (Line.FindChar(',', Comma)) { + Vol = FCString::Atof(*Line.Left(Comma)); + Text = Line.Mid(Comma + 1); + } + + AsyncTask(ENamedThreads::GameThread, [WeakThis, Vol, Text]() { + if (auto *Self = WeakThis.Get()) + Self->OnVoiceInput.Broadcast(Vol, Text); + }); + } + } + }); + + ReaderThread.detach(); +} + +void UDMVOPClient::Disconnect() { + bRunning.store(false); + if (Socket) { + Socket->Close(); + Socket = nullptr; + } +} diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h new file mode 100644 index 0000000..b05f8ce --- /dev/null +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h @@ -0,0 +1,39 @@ +#pragma once + +#include "Async/Async.h" +#include "CoreMinimal.h" +#include "SocketSubsystem.h" +#include "Sockets.h" +#include "UObject/NoExportTypes.h" +#include "UObject/WeakObjectPtr.h" +#include <atomic> +#include <thread> + +#include "DMVOPBridgeClient.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDMVOPVoiceInput, float, Volume, + const FString &, Sentence); + +UCLASS(BlueprintType) +class DMVOPBRIDGE_API UDMVOPClient : public UObject { + GENERATED_BODY() + +public: + UDMVOPClient(); + virtual ~UDMVOPClient(); + + UFUNCTION(BlueprintCallable, Category = "DMVOP") + void Connect(const FString &Host, int32 Port); + + UFUNCTION(BlueprintCallable, Category = "DMVOP") + void Disconnect(); + + UPROPERTY(BlueprintAssignable, Category = "DMVOP") + FOnDMVOPVoiceInput OnVoiceInput; + +private: + FSocket *Socket; + FString PartialLine; + std::thread ReaderThread; + std::atomic<bool> bRunning{false}; +}; diff --git a/binding/unreal/build-plugin.ps1 b/binding/unreal/build-plugin.ps1 new file mode 100644 index 0000000..c52c8bc --- /dev/null +++ b/binding/unreal/build-plugin.ps1 @@ -0,0 +1,72 @@ +param( + [Parameter(Mandatory = $true)] + [string]$UE, + [string]$Output = "", + [switch]$Full +) + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +if ($Output -eq "") { + $Output = Join-Path $ScriptDir "build\DMVOPBridge" +} + +$PluginDir = Join-Path $ScriptDir "DMVOPBridge" +$Plugin = Join-Path $PluginDir "DMVOPBridge.uplugin" +$HostDir = Join-Path $ScriptDir ".host" +$HostPluginDir = Join-Path $HostDir "Plugins\DMVOPBridge" +$HostProj = Join-Path $HostDir "HostProject.uproject" +$UAT = Join-Path $UE "Engine\Build\BatchFiles\RunUAT.bat" +$UBT = Join-Path $UE "Engine\Build\BatchFiles\Build.bat" + +if (-not (Test-Path $UAT)) { + Write-Host "UE not found at $UE" -ForegroundColor Red + exit 1 +} + +# ── Ensure .host project + plugin symlink ── +if (-not (Test-Path $HostPluginDir)) { + New-Item -ItemType Directory -Path (Join-Path $HostDir "Plugins") -Force | Out-Null + cmd /c "mklink /J `"$HostPluginDir`" `"$PluginDir`"" | Out-Null +} + +if (-not (Test-Path $HostProj)) { + $proj = @" +{ + "FileVersion": 3, + "EngineAssociation": "5.6", + "Category": "", + "Description": "", + "Plugins": [ + { "Name": "DMVOPBridge", "Enabled": true } + ] +} +"@ + Set-Content -Path $HostProj -Value $proj +} + +if ($Full -or -not (Test-Path $Output)) { + # ── Full build ── + Write-Host "=== DMVOPBridge (full) ===" -ForegroundColor Cyan + & $UAT BuildPlugin -Plugin="$Plugin" -Package="$Output" -Rocket +} else { + # ── Incremental build ── + Write-Host "=== DMVOPBridge (incremental) ===" -ForegroundColor Cyan + & $UBT UnrealEditor Win64 Development ` + -Project="$HostProj" ` + -Plugin="$Plugin" ` + -TargetType=Editor + + # Copy built DLL to output dir + $DllSrc = "$HostDir\Binaries" + $DllDst = "$Output\Binaries" + if (Test-Path $DllSrc) { + Copy-Item $DllSrc $DllDst -Recurse -Force + Write-Host " Updated: $Output\Binaries" + } +} + +if ($LASTEXITCODE -eq 0) { + Write-Host "=== Done ===" -ForegroundColor Green +} else { + Write-Host "Build failed" -ForegroundColor Red +} |
