aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-06-22 05:21:19 +0800
committer魏曹先生 <1992414357@qq.com>2026-06-22 05:21:19 +0800
commit51c46d43f5c1be332566305b8f8b40ac9041a06a (patch)
treed903b119f2cde121d0e47c5941f07567e44b1c27
parentff5e4faa8082bcaad508021b765e1192462ba5ea (diff)
feat(unreal): add DMVOP TCP client plugin for Unreal Engine
-rw-r--r--README.md17
-rw-r--r--binding/unreal/.gitignore6
-rw-r--r--binding/unreal/DMVOPBridge/DMVOPBridge.uplugin18
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs13
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.cpp2
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.h9
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp93
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h39
-rw-r--r--binding/unreal/build-plugin.ps172
9 files changed, 269 insertions, 0 deletions
diff --git a/README.md b/README.md
index fb35cd6..17a4f28 100644
--- a/README.md
+++ b/README.md
@@ -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
+}