aboutsummaryrefslogtreecommitdiff
path: root/binding
diff options
context:
space:
mode:
Diffstat (limited to 'binding')
-rw-r--r--binding/unreal/DMVOPBridge/DMVOPBridge.uplugin2
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridge.Build.cs3
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.cpp243
-rw-r--r--binding/unreal/DMVOPBridge/Source/DMVOPBridge/DMVOPBridgeClient.h23
4 files changed, 114 insertions, 157 deletions
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 <winsock2.h>
-#include <ws2tcpip.h>
-// Undef Windows macros that conflict with UE types
-#ifdef SetPort
-#undef SetPort
-#endif
-#endif
+#include "HAL/PlatformMisc.h"
// ═════════════════════════════════════════════════════════════════════════════
// FDMVOPWorker
// ═════════════════════════════════════════════════════════════════════════════
-FDMVOPWorker::FDMVOPWorker(TWeakObjectPtr<UDMVOPClient> InOwner, FString InHost,
- int32 InPort)
- : bRun(false), Socket(nullptr), Owner(InOwner), Host(MoveTemp(InHost)),
- Port(InPort), Thread(nullptr) {}
+FDMVOPWorker::FDMVOPWorker(TWeakObjectPtr<UDMVOPClient> 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<UDMVOPClient> WeakOwner = Owner;
+ AsyncTask(ENamedThreads::GameThread, [WeakOwner]() {
+ if (auto *Self = WeakOwner.Get())
+ UE_LOG(LogTemp, Error, TEXT("DMVOP: Failed to create pipes"));
+ });
return 0;
}
- TSharedRef<FInternetAddr> 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<UDMVOPClient> 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<UDMVOPClient> 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<uint8> 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<const char *>(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 <atomic>
#include "DMVOPBridgeClient.generated.h"
@@ -18,8 +16,8 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnDMVOPVoiceInput, float, Volume,
class FDMVOPWorker : public FRunnable {
public:
- FDMVOPWorker(TWeakObjectPtr<class UDMVOPClient> InOwner, FString InHost,
- int32 InPort);
+ FDMVOPWorker(TWeakObjectPtr<class UDMVOPClient> 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<UDMVOPClient> 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<FDMVOPWorker> Worker;
- FString PartialLine;
};