From 84d881b05b0f624e9094aa3e2d61b5bd4a764ea9 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 2 Jul 2026 22:49:04 +0800 Subject: feat(unreal): switch dmvop integration from TCP client to subprocess launch --- binding/unreal/DMVOPBridge/DMVOPBridge.uplugin | 2 +- .../Source/DMVOPBridge/DMVOPBridge.Build.cs | 3 +- .../Source/DMVOPBridge/DMVOPBridgeClient.cpp | 243 +++++++++------------ .../Source/DMVOPBridge/DMVOPBridgeClient.h | 23 +- 4 files changed, 114 insertions(+), 157 deletions(-) (limited to 'binding') diff --git a/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin b/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin index 349425a..4c54314 100644 --- a/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin +++ b/binding/unreal/DMVOPBridge/DMVOPBridge.uplugin @@ -3,7 +3,7 @@ "Version": 0, "VersionName": "0.1.0", "FriendlyName": "Dumb Voice Protocol - UE Binding", - "Description": "TCP client for dmvop voice input in Unreal Engine", + "Description": "Launch dmvop as subprocess for voice input in Unreal Engine", "Category": "Input", "CreatedBy": "Weicao-CatilGrass", "CreatedByURL": "https://github.com/catilgrass/dumb-voice-protocol", diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs index 5bc7d15..35944ec 100644 --- a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs @@ -6,8 +6,7 @@ public class DMVOPBridge : ModuleRules { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { - "Core", "CoreUObject", "Engine", - "Sockets", "Networking" + "Core", "CoreUObject", "Engine" }); } } diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp index 8a436e0..965f7a8 100644 --- a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp @@ -1,24 +1,15 @@ #include "DMVOPBridgeClient.h" -#include "IPAddress.h" -#include "Interfaces/IPv4/IPv4Address.h" - -#ifdef _WIN32 -#include -#include -// Undef Windows macros that conflict with UE types -#ifdef SetPort -#undef SetPort -#endif -#endif +#include "HAL/PlatformMisc.h" // ═════════════════════════════════════════════════════════════════════════════ // FDMVOPWorker // ═════════════════════════════════════════════════════════════════════════════ -FDMVOPWorker::FDMVOPWorker(TWeakObjectPtr InOwner, FString InHost, - int32 InPort) - : bRun(false), Socket(nullptr), Owner(InOwner), Host(MoveTemp(InHost)), - Port(InPort), Thread(nullptr) {} +FDMVOPWorker::FDMVOPWorker(TWeakObjectPtr InOwner, + FString InDmvopPath, FString InArgs) + : bRun(false), ReadPipe(nullptr), WritePipe(nullptr), Owner(InOwner), + DmvopPath(MoveTemp(InDmvopPath)), Args(MoveTemp(InArgs)), + Thread(nullptr) {} FDMVOPWorker::~FDMVOPWorker() { Stop(); @@ -30,9 +21,8 @@ FDMVOPWorker::~FDMVOPWorker() { } void FDMVOPWorker::Start() { - Thread = FRunnableThread::Create( - this, *FString::Printf(TEXT("DMVOP %s:%d"), *Host, Port), 128 * 1024, - TPri_Normal); + Thread = FRunnableThread::Create(this, TEXT("DMVOP Stdout"), 128 * 1024, + TPri_Normal); } bool FDMVOPWorker::Init() { @@ -41,131 +31,57 @@ bool FDMVOPWorker::Init() { } uint32 FDMVOPWorker::Run() { - // ── Create socket ── - Socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM) - ->CreateSocket(NAME_Stream, TEXT("DMVOP"), false); - if (!Socket) { - return 0; - } - - int32 RecvSize = 0, SendSize = 0; - Socket->SetReceiveBufferSize(16384, RecvSize); - Socket->SetSendBufferSize(16384, SendSize); - - // ── Resolve address ── - FIPv4Address AddrIP; - if (!FIPv4Address::Parse(Host, AddrIP)) { - Socket->Close(); - delete Socket; - Socket = nullptr; + // ── Create pipes ── + if (!FPlatformProcess::CreatePipe(ReadPipe, WritePipe)) { + TWeakObjectPtr WeakOwner = Owner; + AsyncTask(ENamedThreads::GameThread, [WeakOwner]() { + if (auto *Self = WeakOwner.Get()) + UE_LOG(LogTemp, Error, TEXT("DMVOP: Failed to create pipes")); + }); return 0; } - TSharedRef DstAddr = - ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr(); - DstAddr->SetIp(AddrIP.Value); - DstAddr->SetPort(Port); + // ── Launch dmvop process ── + // PipeWriteChild = WritePipe (child writes stdout here), + // PipeReadChild = nullptr (we don't write to child's stdin). + ProcessHandle = + FPlatformProcess::CreateProc(*DmvopPath, *Args, false, true, true, + nullptr, 0, nullptr, WritePipe, nullptr); - // ── Connect ── - if (!Socket->Connect(*DstAddr)) { + if (!ProcessHandle.IsValid()) { + FPlatformProcess::ClosePipe(ReadPipe, WritePipe); + ReadPipe = WritePipe = nullptr; TWeakObjectPtr WeakOwner = Owner; AsyncTask(ENamedThreads::GameThread, [WeakOwner]() { if (auto *Self = WeakOwner.Get()) - UE_LOG(LogTemp, Error, TEXT("DMVOP: Failed to connect")); + UE_LOG(LogTemp, Error, TEXT("DMVOP: Failed to launch dmvop process")); }); - Socket->Close(); - delete Socket; - Socket = nullptr; return 0; } TWeakObjectPtr WeakOwner = Owner; AsyncTask(ENamedThreads::GameThread, [WeakOwner]() { if (auto *Self = WeakOwner.Get()) - UE_LOG(LogTemp, Log, TEXT("DMVOP: Connected")); + UE_LOG(LogTemp, Log, TEXT("DMVOP: Process started")); }); - // ── Main loop ── - TArray Buf; - Buf.SetNumUninitialized(4096); + // ── Main read loop ── FString Partial; - -#ifdef _WIN32 - // Raw socket for comparison (same destination, bypasses UE layer) - SOCKET RawSock = INVALID_SOCKET; - bool bRawOK = false; - RawSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); - if (RawSock != INVALID_SOCKET) { - struct sockaddr_in RawAddr; - FMemory::Memzero(&RawAddr, sizeof(RawAddr)); - RawAddr.sin_family = AF_INET; - RawAddr.sin_port = htons(Port); - inet_pton(AF_INET, TCHAR_TO_UTF8(*Host), &RawAddr.sin_addr); - if (connect(RawSock, (struct sockaddr *)&RawAddr, sizeof(RawAddr)) == 0) { - bRawOK = true; - u_long Mode = 1; - ioctlsocket(RawSock, FIONBIO, &Mode); - UE_LOG(LogTemp, Log, TEXT("DMVOP: Raw socket connected for comparison")); - } else { - closesocket(RawSock); - RawSock = INVALID_SOCKET; - } - } -#endif - while (bRun) { - FDateTime TickStart = FDateTime::UtcNow(); - - // Peek check - Socket->SetNonBlocking(true); - int32 DummyRead = 0; - uint8 Dummy; - bool bPeekOK = - Socket->Recv(&Dummy, 1, DummyRead, ESocketReceiveFlags::Peek); - Socket->SetNonBlocking(false); - - if (!bPeekOK) { + // Check if process is still alive + if (!FPlatformProcess::IsProcRunning(ProcessHandle)) { + // One last read to drain remaining output + FString LastChunk = FPlatformProcess::ReadPipe(ReadPipe); + Partial += LastChunk; break; } - // Receive data - while (bRun) { - uint32 PendingSize = 0; - bool bHasData = Socket->HasPendingData(PendingSize) && PendingSize > 0; - -#ifdef _WIN32 - // Compare: does raw socket have data when UE socket doesn't? - bool bRawHasData = false; - if (bRawOK) { - char RawBuf[1]; - int RawRead = recv(RawSock, RawBuf, 1, MSG_PEEK); - bRawHasData = (RawRead > 0); - if (bHasData != bRawHasData) { - static int LogCount = 0; - if (++LogCount <= 5) { - UE_LOG(LogTemp, Log, TEXT("DMVOP: COMPARE UE=%d Raw=%d (Peek=%d)"), - bHasData, bRawHasData, RawRead); - } - } - } -#endif - - if (!bHasData) { - break; - } - - Buf.SetNumUninitialized(FMath::Max(PendingSize, 4096u)); - int32 ReadNow = 0; - if (!Socket->Recv(Buf.GetData(), (int32)PendingSize, ReadNow, - ESocketReceiveFlags::None) || - ReadNow <= 0) { - break; - } - - Partial += - FString(ReadNow, - UTF8_TO_TCHAR(reinterpret_cast(Buf.GetData()))); + // Read available data from stdout + FString Chunk = FPlatformProcess::ReadPipe(ReadPipe); + if (!Chunk.IsEmpty()) { + Partial += Chunk; + // Parse complete lines int32 Idx; while (Partial.FindChar('\n', Idx)) { FString Line = Partial.Left(Idx).TrimEnd(); @@ -189,30 +105,53 @@ uint32 FDMVOPWorker::Run() { } } - // Sleep - FTimespan Elapsed = FDateTime::UtcNow() - TickStart; - float SleepSec = 0.008f - (float)Elapsed.GetTotalSeconds(); - if (SleepSec > 0.0f) { - FPlatformProcess::Sleep(SleepSec); - } + FPlatformProcess::Sleep(0.008f); } - // ── Cleanup ── -#ifdef _WIN32 - if (bRawOK) { - closesocket(RawSock); + // ── Process exited: flush remaining partial line ── + if (!Partial.IsEmpty()) { + FString Line = Partial.TrimEnd(); + if (!Line.IsEmpty()) { + float Vol = 0.0f; + FString Text = Line; + int32 Comma; + if (Line.FindChar(',', Comma)) { + Vol = FCString::Atof(*Line.Left(Comma)); + Text = Line.Mid(Comma + 1); + } + WeakOwner = Owner; + AsyncTask(ENamedThreads::GameThread, [WeakOwner, Vol, Text]() { + if (auto *Self = WeakOwner.Get()) + Self->DispatchVoiceInput(Vol, Text); + }); + } } -#endif - if (Socket) { - Socket->Close(); - delete Socket; - Socket = nullptr; - } + // ── Wait for exit and clean up ── + int32 ExitCode = 0; + FPlatformProcess::GetProcReturnCode(ProcessHandle, &ExitCode); + FPlatformProcess::CloseProc(ProcessHandle); + ProcessHandle.Reset(); + + FPlatformProcess::ClosePipe(ReadPipe, WritePipe); + ReadPipe = WritePipe = nullptr; + + WeakOwner = Owner; + AsyncTask(ENamedThreads::GameThread, [WeakOwner, ExitCode]() { + if (auto *Self = WeakOwner.Get()) + UE_LOG(LogTemp, Log, TEXT("DMVOP: Process exited with code %d"), + ExitCode); + }); + return 0; } -void FDMVOPWorker::Stop() { bRun = false; } +void FDMVOPWorker::Stop() { + bRun = false; + if (ProcessHandle.IsValid()) { + FPlatformProcess::TerminateProc(ProcessHandle, true); + } +} // ═════════════════════════════════════════════════════════════════════════════ // UDMVOPClient @@ -226,17 +165,35 @@ UDMVOPClient::~UDMVOPClient() { } } -void UDMVOPClient::Connect(const FString &Host, int32 Port) { +void UDMVOPClient::StartDmvop(const FString &DmvopPath, + const FString &ExtraArgs) { if (Worker.IsValid()) { - UE_LOG(LogTemp, Warning, TEXT("DMVOP: Already connected")); + UE_LOG(LogTemp, Warning, TEXT("DMVOP: Already running")); return; } - UE_LOG(LogTemp, Log, TEXT("DMVOP: Connecting to %s:%d..."), *Host, Port); - Worker = MakeShareable(new FDMVOPWorker(this, Host, Port)); + + // Resolve executable path: arg > env var > fallback to PATH + FString ExePath = DmvopPath; + if (ExePath.IsEmpty()) { + FString EnvPath = FPlatformMisc::GetEnvironmentVariable(TEXT("DMVOP_PATH")); + if (!EnvPath.IsEmpty()) { + ExePath = EnvPath; + } else { + ExePath = TEXT("dmvop"); + } + } + + // Ensure stdout output mode and no-confirm flags + FString Args = TEXT("--output stdout --no-confirm"); + if (!ExtraArgs.IsEmpty()) { + Args += TEXT(" ") + ExtraArgs; + } + UE_LOG(LogTemp, Log, TEXT("DMVOP: Launching %s %s"), *ExePath, *Args); + Worker = MakeShareable(new FDMVOPWorker(this, ExePath, Args)); Worker->Start(); } -void UDMVOPClient::Disconnect() { +void UDMVOPClient::StopDmvop() { if (Worker.IsValid()) { Worker.Reset(); } diff --git a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h index 63542c7..dd36242 100644 --- a/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h +++ b/binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h @@ -2,14 +2,12 @@ #include "Async/Async.h" #include "CoreMinimal.h" +#include "HAL/PlatformProcess.h" #include "HAL/Runnable.h" #include "HAL/RunnableThread.h" #include "HAL/ThreadSafeBool.h" -#include "SocketSubsystem.h" -#include "Sockets.h" #include "UObject/NoExportTypes.h" #include "UObject/WeakObjectPtr.h" -#include #include "DMVOPBridgeClient.generated.h" @@ -18,8 +16,8 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDMVOPVoiceInput, float, Volume, class FDMVOPWorker : public FRunnable { public: - FDMVOPWorker(TWeakObjectPtr InOwner, FString InHost, - int32 InPort); + FDMVOPWorker(TWeakObjectPtr InOwner, FString InDmvopPath, + FString InArgs); virtual ~FDMVOPWorker(); void Start(); @@ -31,10 +29,12 @@ public: private: FThreadSafeBool bRun; - FSocket *Socket; + void *ReadPipe; + void *WritePipe; + FProcHandle ProcessHandle; TWeakObjectPtr Owner; - FString Host; - int32 Port; + FString DmvopPath; + FString Args; FRunnableThread *Thread; }; @@ -46,11 +46,13 @@ public: UDMVOPClient(); virtual ~UDMVOPClient(); + /** Launch dmvop as a subprocess and read its stdout */ UFUNCTION(BlueprintCallable, Category = "DMVOP") - void Connect(const FString &Host, int32 Port); + void StartDmvop(const FString &DmvopPath, + const FString &ExtraArgs = TEXT("")); UFUNCTION(BlueprintCallable, Category = "DMVOP") - void Disconnect(); + void StopDmvop(); UPROPERTY(BlueprintAssignable, Category = "DMVOP") FOnDMVOPVoiceInput OnVoiceInput; @@ -59,5 +61,4 @@ public: private: TSharedPtr Worker; - FString PartialLine; }; -- cgit