Skip to content

Commit

Permalink
Hook SpVoice to fix narrator (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
modmuss50 authored May 25, 2024
1 parent 9618f73 commit 6ba9280
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 1 deletion.
44 changes: 44 additions & 0 deletions windows/Sources/Hook/Hook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
#include <Detours.h>
#include <string>
#include <stdexcept>
#include <sapi.h>
#include <AtlBase.h>
#include <AtlConv.h>
#include <AtlCom.h>

using namespace std::string_literals;
#pragma clang diagnostic ignored "-Wmicrosoft-cast"

#define VTABLE_INDEX_SPEAK 20
#define VTABLE_INDEX_SKIP 23

// Workaround for GetVolumeInformationW not working in a UWP application
// This is by creating a handle to a file on the drive and then using GetVolumeInformationByHandleW with that handle
static BOOL (WINAPI* TrueGetVolumeInformationW)(LPCWSTR, LPWSTR, DWORD, LPDWORD, LPDWORD, LPDWORD, LPWSTR, DWORD) = GetVolumeInformationW;
Expand Down Expand Up @@ -93,6 +100,39 @@ BOOL WINAPI SetCursorPosPatch(int x, int y) {
return true;
}

HRESULT __stdcall SpeakPatch(ISpVoice* This, LPCWSTR pwcs, DWORD dwFlags, ULONG *pulStreamNumber) {
CW2A utf8(pwcs, CP_UTF8);
Runtime::speak(utf8.m_psz, dwFlags);
return S_OK;
}

HRESULT __stdcall SpeakSkipPatch(ISpVoice* This, LPCWSTR *pItemType, long lItems, ULONG *pulNumSkipped) {
Runtime::speakSkip();
return S_OK;
}

struct _ISpVoiceVTable {
void* speak;
void* skip;
};

_ISpVoiceVTable createISpVoiceVTable() {
CoInitializeEx(nullptr, 0);

CComPtr<ISpVoice> spVoice;
if (!SUCCEEDED(spVoice.CoCreateInstance(CLSID_SpVoice))) {
throw std::runtime_error("Failed to create ISpVoice instance");
}

auto vTable = *(void***)spVoice.p;
void* speak = vTable[VTABLE_INDEX_SPEAK];
void* skip = vTable[VTABLE_INDEX_SKIP];

return {speak, skip};
}

static _ISpVoiceVTable spVoiceVTable = createISpVoiceVTable();

BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved) {
if (DetourIsHelperProcess()) {
return true;
Expand All @@ -106,13 +146,17 @@ BOOL WINAPI DllMain(HINSTANCE hinst, DWORD dwReason, LPVOID reserved) {
DetourAttach(&(PVOID&)TrueGetVolumeInformationW, GetVolumeInformationWPatch);
DetourAttach(&(PVOID&)TrueClipCursor, ClipCursorPatch);
DetourAttach(&(PVOID&)TrueSetCursorPos, SetCursorPosPatch);
DetourAttach(&(PVOID&)spVoiceVTable.speak, SpeakPatch);
DetourAttach(&(PVOID&)spVoiceVTable.skip, SpeakSkipPatch);
DetourTransactionCommit();
} else if (dwReason == DLL_PROCESS_DETACH) {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)TrueGetVolumeInformationW, GetVolumeInformationWPatch);
DetourDetach(&(PVOID&)TrueClipCursor, ClipCursorPatch);
DetourDetach(&(PVOID&)TrueSetCursorPos, SetCursorPosPatch);
DetourDetach(&(PVOID&)spVoiceVTable.speak, SpeakPatch);
DetourDetach(&(PVOID&)spVoiceVTable.skip, SpeakSkipPatch);
DetourTransactionCommit();
Runtime::processDetach();
}
Expand Down
1 change: 1 addition & 0 deletions windows/Sources/SandboxTest/SandboxTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SandboxTest {
"namedPipe": NamedPipeCommand(),
"nameMax": NameMaxCommand(),
"mouseMovements": MouseMovementsCommand(),
"speech": SpeechCommand(),
]

static func main() throws {
Expand Down
36 changes: 36 additions & 0 deletions windows/Sources/SandboxTest/SpeechCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import WinSDK
import WindowsUtils
import WinSDKExtras
import CxxStdlib

// Enable to run the full test, with a long message that will get cut off.
let FULL_TEST = false

// Use the win32 Speech API to convert text to speech
class SpeechCommand: Command {
private static let SPF_ASYNC = DWORD(1)
private static let SPF_IS_NOT_XML = DWORD(1 << 4)

func execute(_ arguments: [String]) throws {
CoInitializeEx(nil, 0)
defer {
CoUninitialize()
}

var speak = SpeakApi()
let flags: DWORD = SpeechCommand.SPF_ASYNC | SpeechCommand.SPF_IS_NOT_XML
let message = FULL_TEST ? "Hello Fabric, Sandbox. This is a long message that will get cut off" : ""
let result = speak.Speak(std.string(message), flags)
guard result == S_OK else {
throw Win32Error("Failed to speak", result: result)
}

if FULL_TEST {
Sleep(2000)
}

speak.Skip()

print("Spoke")
}
}
15 changes: 14 additions & 1 deletion windows/Tests/IntergrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,14 @@ import WindowsUtils
["mouseMovements", "-Dsandbox.namedPipe=\(server.path)"], namedPipe: server)
#expect(exitCode == 0)
}

@Test func testSpeech() throws {
let server = try SandboxNamedPipeServer(
pipeName: "\\\\.\\pipe\\FabricSandbox" + randomString(length: 10))
let (exitCode, output) = try runIntergration(["speech"], namedPipe: server)
#expect(exitCode == 0)
#expect(output == "Spoke")
}
}
func runIntergration(
_ args: [String], capabilities: [SidCapability] = [], filePermissions: [FilePermission] = [],
Expand Down Expand Up @@ -164,14 +172,19 @@ func runIntergration(
accessPermissions: [.genericRead, .genericExecute])
}

var commandLine = [testExecutable.path()] + args

if let namedPipe = namedPipe {
try grantNamedPipeAccess(
pipe: namedPipe, appContainer: container, accessPermissions: [.genericRead, .genericWrite])
if namedPipe is SandboxNamedPipeServer {
commandLine.append("-Dsandbox.namedPipe=\(namedPipe.path)")
}
}

let outputConsumer = TestOutputConsumer()
let process = SandboxedProcess(
application: testExecutable, commandLine: [testExecutable.path()] + args,
application: testExecutable, commandLine: commandLine,
workingDirectory: workingDirectory, container: container, outputConsumer: outputConsumer)
let exitCode = try process.run()
return (exitCode, outputConsumer.trimmed())
Expand Down

0 comments on commit 6ba9280

Please sign in to comment.