Winelib SAPI4 host: MSTTS (Sam/Mary/Mike) + AT&T Flextalk (Keith Bell/Kerry Watson)

This commit is contained in:
Fisto
2026-09-15 11:31:02 +02:00
commit 516f0dbdf3
10 changed files with 857 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
build/
*.o
*.a
*.so
*.exe
*.dll
setup/out-*.reg
*.wav
+84
View File
@@ -0,0 +1,84 @@
# MicroslopSam
Run SAPI4 text-to-speech voices on Linux.
Primary approach: a **Winelib host** — a native Linux ELF that uses Wine's
libraries to load the original 32-bit SAPI4 runtime and TTS engine DLLs.
Voice targets:
- Microsoft Sam / Mary / Mike — MSTTS engine (`msttssyn.dll` + `.vce` data)
- AT&T Flextalk 2.1 — "Keith Bell" and "Kerry Watson" (`FlexTalk.dll`)
Long-term: replace Wine-provided pieces with own shims (rehost) for a
pure-native build, and keep a path to a fully portable engine.
## Status (working)
- Winelib host builds and runs: `build/sapi4host.exe` (launcher) +
`build/sapi4host.exe.so` (native 32-bit ELF).
- Voices enumerate and synthesize:
- MSTTS: Sam, Mary, Mike (+ telephone and effect variants)
- AT&T Flextalk: Keith Bell, Kerry Watson
- Test output: `build/test_sam.wav` (22.05 kHz), `build/test_Keith_Bell.wav`,
`build/test_Kerry_Watson.wav` (11.025 kHz).
## Build
Dependencies (once, as root):
sudo setup/install-deps.sh
Runtime + MSTTS voices (once):
setup/setup-prefix.sh
Flextalk voices (once; needs the extracted installer, see `docs/NOTES.md`):
setup/setup-flextalk.sh
Host:
# Winelib (native ELF + launcher)
winegcc -m32 -O2 -o build/sapi4host host/sapi4host.c -I../reference \
-lole32 -luser32 -ladvapi32
# PE fallback (runs under wine):
i686-w64-mingw32-gcc -O2 -static -o build/sapi4host.exe host/sapi4host.c \
-I../reference -lole32 -luser32 -ladvapi32 -luuid
## Usage
setup/run.sh -l
setup/run.sh -v Sam -t "Hello." -o sam.wav
setup/run.sh -v "Keith Bell" -t "Hello." -o keith.wav -p 100 -s 150
Options: `-v` voice, `-t` text, `-o` output WAV, `-p` pitch, `-s` speed.
## Layout
- `host/sapi4host.c` — host: voice select, pitch/speed, text -> WAV
- `setup/install-deps.sh` — Wine/Winelib/helpers (root)
- `setup/setup-prefix.sh` — prefix + SAPI4 runtime + MSTTS engine/voices
- `setup/setup-flextalk.sh` — Flextalk files + registry
- `setup/inf2reg.py` — INF AddReg -> `.reg` converter
- `setup/run.sh` — run helper (prefix + Xvfb)
- `docs/NOTES.md` — registry/CLSID facts, runtime RE notes, gotchas
## Known issues
- `xvfb-run` is flaky on this host. `setup/run.sh` starts a persistent Xvfb
(`:77`) instead; Wine needs a working X display because the runtime creates
windows.
- Flextalk's default pitch/speed report as maximum (250/390) through
`ITTSAttributes`; values are still adjustable with `-p`/`-s`.
- The Flextalk archive lacks `data2.cab`; `unshield -O` extracts `data1.cab`
but one tail file (`WatsonUNInstall.dll`) is incomplete.
## Reference material (not in this repo)
- `../reference/speech.h` — SAPI4 SDK header
- `../reference/SAPI4-CLI`, `../reference/TETYYS-SAPI4` — host examples
- `../Microsoft-Sam-Mary-Mike-TruVoice-WSAPI4` — installers/payloads
- `../flextalk` — Flextalk installer archive + extraction
- `/mnt/ghidra/decomp/{msttssyn.dll,speech.dll}/` — decompiled corpora
+26
View File
@@ -0,0 +1,26 @@
# Scope
## Goals
1. Synthesize speech with the MSTTS voices (Sam, Mary, Mike) on Linux.
2. Synthesize speech with the AT&T Flextalk 2.1 voices ("Keith Bell",
"Kerry Watson") on Linux, through the same host.
3. Ship as a native Linux binary (Winelib first, pure-native later).
4. Keep the engine contract limited to what `speech.h` defines, so other
SAPI4 engines can be added without host changes.
5. Text in, WAV out. Pitch and speed adjustable.
## Approach
- Phase 1 (current): Winelib host + Wine-provided runtime. Fastest path to
working audio, and it doubles as the reference oracle for later work.
- Phase 2: replace Wine-provided parts with own shims one at a time
(file I/O, threads, OLE2, window/message pump, finally MSVC EH/SEH).
- Phase 3: pure-native rehost of `msttssyn.dll`; no Wine libraries.
## Non-goals (for now)
- GUI, SAPI4 telephony (`spchtel.dll`), Voice Command (`vcmd`), Agent support.
- TruVoice voices (possible later; same L&H engine family).
- 64-bit build (the engines are 32-bit).
- Multi-platform audio (PipeWire/ALSA output comes after WAV works).
+77
View File
@@ -0,0 +1,77 @@
# Notes
## Registry facts
From `spchapi/SPCHAPI.INF` (`%49000%` = `%25%\speech` = `C:\windows\speech`):
- `CLSID_TTSEnumerator` `{D67C0280-C743-11cd-80E5-00AA003E4B50}` -> `Speech.dll`
- "Audio Destination to File Object" `{D4623720-E4B9-11cf-8D56-00A0C9034A7E}` -> `Speech.dll`
- "Audio Destination Object" `{CB96B400-C743-11cd-80E5-00AA003E4B50}` -> `Speech.dll`
- Lexicon dialog `{92655FB1-ADF9-11d1-BEB9-006008317CE8}` -> `Speech.dll`
- Phoneme converter `{B9F11A96-90E3-11d0-8D77-00A0C9034A7E}` -> `Speech.dll`
From `msttsl/mstts.inf` (`[TTSReg]`):
- Engine `MSTTSSyn` = `{E0725551-286F-11d0-8E73-00A0C9083363}` -> `MSTTSSYN.dll`
- Engine install dir: `Common Files\microsoft shared\SpeechEngines\TTS`.
- Enumerates voices by scanning `*.vce` in its own directory.
## Flextalk registration (from the installer's `setup.ins`)
- Engine value: `HKLM\Software\Voice\TextToSpeech\Engine`
`"AT&T FlexTalk" = "{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}"`
- Engine subkey (forces mode enumeration): `...\Engine\{FFE78E00-...}` with
`Dirty = 1` (REG_DWORD). The runtime rewrites `Count` + `Mode0..N` and
clears `Dirty` after enumerating.
- `HKCR\CLSID\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}\InprocServer32` =
`<root>\EngineDLLs\FlexTalk.dll`, `ThreadingModel = Apartment`.
Also registered: `{778EAAE2-499F-11CF-B0BA-444553540000}` -> same DLL.
- Directories: `HKLM\Software\AT&T\Watson\2.0\FlexTalk\2.0\Directories`
`RootDirectory = C:\FlexTalk\`.
- Dictionaries: `...\FlexTalk\2.0\Dictionaries`:
`StandardDictionary="2 ft_sdict.dll"`, `ExceptionDictionary="0 ft_udict.dll"`,
`HomographDictionary="1 ft_hgrap.dll"`, `ProperNameDictionary="3 ft_namsa.dll"`,
`LetterToSoundDictionary="4 ft_l2s.dll"`.
- Options: `...\FlexTalk\2.0\Options` `Gender=Male`, `SampleRate=11025`.
- Speakers (per-voice settings; names come from these values):
- `{11260610-C84A-11ce-AEC4-0000C0E9CB87}`: Mode Name / Speaker Name
"Keith Bell", Pitch 27, Speaking Rate 33, Head Size 46, Expression 59,
Lowering 40, Prominences 17, Age 30, Volume 100.
- `{000031CB-3699-551F-AED0-6B4893382D57}`: "Kerry Watson", Pitch 65,
Speaking Rate 32, Head Size 45, Expression 100, Lowering 40,
Prominences 17, Age 30, Volume 100.
- Voice data: `EngineData\AE1M08LC.*`, `AE1M11LC.*` (male, 8/11 kHz) and
`AE2F08LC.*`, `AE2F11LC.*` (female); `.INV` inventory, `.NDX` index,
`.COD` codebook, plus shared `DICT_CI.*`, `phondef`, `AMPRULES`, `ENDINGS`.
Extraction: `unshield -O -d extracted x installer/flextalk/data1.cab`
(`-O` = old compression; the archive.org package has no `data2.cab`, so the
final file `WatsonUNInstall.dll` is incomplete).
## SAPI4 runtime engine discovery (`speech.dll`)
- `FUN_635747a4` reads `HKLM\Software\Voice\TextToSpeech\Engine` values
(value name = engine name, data = CLSID string), validates the GUID with
`FUN_635712e4`, then opens a *subkey named by the CLSID string*.
- `FUN_635758b1` (TTS mode-list ctor) reads `Dirty`; when non-zero/missing it
calls `CoCreateInstance(CLSID, ..., IID @635791c0)` and pulls modes via
vtable+0xC into 0xaf0-byte records, writes `Mode%d` (REG_BINARY), `Count`,
then clears `Dirty`. When `Dirty=0` it reads the cached `Mode%d` blobs.
- Practical rule: to add an engine, register the `Engine` value + the CLSID
`InprocServer32` + an engine subkey with `Dirty=1` (a missing subkey also
triggers enumeration).
## Environment gotchas
- Wine needs a working X display (the runtime creates windows). `xvfb-run -a`
proved flaky here; `setup/run.sh` starts a persistent `Xvfb :77` instead.
- Wine reports `err:ole:apartment_add_dll couldn't load in-process dll` when
the `InprocServer32` path does not exist. Check the file copy first
(this bit us: a bad `unshield` run created empty directories).
- 32-bit engines require a 32-bit prefix (`WINEARCH=win32`).
- `sudo` on this machine requires a password; installs are manual.
## Decompiled corpora (Ghidra)
- `/mnt/ghidra/decomp/msttssyn.dll/` (786 functions, engine)
- `/mnt/ghidra/decomp/speech.dll/` (1030 functions, SAPI4 runtime)
+339
View File
@@ -0,0 +1,339 @@
/*
* sapi4host.c — minimal SAPI4 text-to-speech host.
*
* Builds as:
* - Winelib (native Linux ELF): winegcc -m32 -o sapi4host sapi4host.c ...
* - PE (runs under wine): i686-w64-mingw32-gcc -o sapi4host.exe ...
*
* Usage:
* sapi4host -l
* sapi4host -v <voice> -t <text> -o <out.wav> [-p <pitch>] [-s <speed>]
*
* Flow (see reference/speech.h):
* CLSID_TTSEnumerator -> ITTSFind::Find/Select
* CLSID_AudioDestFile -> IAudioFile::Set (WAV)
* ITTSAttributes::PitchSet/SpeedSet
* ITTSNotifySink -> wait for AudioStop
* ITTSCentral::AudioReset / TextData
*
* Derived from the documented SAPI4 flow used by GaryOderNichts/SAPI4-CLI
* and TETYYS/SAPI4 (both small, permissive examples).
*/
#define WIN32_LEAN_AND_MEAN
#include <initguid.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mmsystem.h>
#include <objbase.h>
#include <speech.h>
static volatile LONG g_done = 0;
static volatile LONG g_started = 0;
/* ------------------------------------------------------------------ */
/* ITTSNotifySink */
/* ------------------------------------------------------------------ */
static HRESULT STDMETHODCALLTYPE N_QueryInterface(ITTSNotifySinkA *This,
REFIID riid, LPVOID *ppv)
{
*ppv = NULL;
if (IsEqualIID(riid, &IID_IUnknown) ||
IsEqualIID(riid, &IID_ITTSNotifySink)) {
*ppv = (LPVOID)This;
return S_OK;
}
return E_NOINTERFACE;
}
static ULONG STDMETHODCALLTYPE N_AddRef(ITTSNotifySinkA *This)
{
(void)This;
return 1;
}
static ULONG STDMETHODCALLTYPE N_Release(ITTSNotifySinkA *This)
{
(void)This;
return 1;
}
static HRESULT STDMETHODCALLTYPE N_AttribChanged(ITTSNotifySinkA *This, DWORD id)
{
(void)This; (void)id;
return S_OK;
}
static HRESULT STDMETHODCALLTYPE N_AudioStart(ITTSNotifySinkA *This, QWORD ts)
{
(void)This; (void)ts;
InterlockedExchange(&g_started, 1);
return S_OK;
}
static HRESULT STDMETHODCALLTYPE N_AudioStop(ITTSNotifySinkA *This, QWORD ts)
{
(void)This; (void)ts;
InterlockedExchange(&g_done, 1);
return S_OK;
}
static HRESULT STDMETHODCALLTYPE N_Visual(ITTSNotifySinkA *This, QWORD ts,
CHAR ipa, CHAR engine,
DWORD hints, PTTSMOUTH mouth)
{
(void)This; (void)ts; (void)hints; (void)mouth;
if (ipa)
fprintf(stderr, "phoneme ipa=0x%02x engine=0x%02x\n",
(unsigned char)ipa, (unsigned char)engine);
return S_OK;
}
static ITTSNotifySinkAVtbl g_notifyVtbl = {
N_QueryInterface,
N_AddRef,
N_Release,
N_AttribChanged,
N_AudioStart,
N_AudioStop,
N_Visual
};
static ITTSNotifySinkA g_notify = { &g_notifyVtbl };
/* ------------------------------------------------------------------ */
/* helpers */
/* ------------------------------------------------------------------ */
static void list_voices(void)
{
PITTSENUM e = NULL;
TTSMODEINFO info;
HRESULT hr = CoCreateInstance(&CLSID_TTSEnumerator, NULL, CLSCTX_ALL,
&IID_ITTSEnum, (void **)&e);
if (FAILED(hr) || !e) {
fprintf(stderr, "cannot create TTS enumerator (0x%08lx)\n",
(unsigned long)hr);
return;
}
for (;;) {
memset(&info, 0, sizeof(info));
if (e->lpVtbl->Next(e, 1, &info, NULL) != S_OK)
break;
printf("%s\n", info.szModeName);
}
e->lpVtbl->Release(e);
}
static HRESULT find_voice(const char *name, TTSMODEINFO *out)
{
PITTSFIND f = NULL;
TTSMODEINFO want, got;
HRESULT hr;
hr = CoCreateInstance(&CLSID_TTSEnumerator, NULL, CLSCTX_ALL,
&IID_ITTSFind, (void **)&f);
if (FAILED(hr) || !f)
return hr;
memset(&want, 0, sizeof(want));
strncpy(want.szModeName, name, sizeof(want.szModeName) - 1);
memset(&got, 0, sizeof(got));
hr = f->lpVtbl->Find(f, &want, NULL, &got);
f->lpVtbl->Release(f);
if (FAILED(hr))
return hr;
*out = got;
return S_OK;
}
static void usage(void)
{
fprintf(stderr,
"usage: sapi4host -l\n"
" sapi4host -v <voice> -t <text> -o <out.wav>"
" [-p pitch] [-s speed]\n");
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
int main(int argc, char **argv)
{
const char *voice = NULL, *text = NULL, *out = NULL;
int pitch = -1, speed = -1;
int i;
TTSMODEINFO mode;
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--list")) {
CoInitialize(NULL);
list_voices();
CoUninitialize();
return 0;
} else if (!strcmp(argv[i], "-v")) voice = argv[++i];
else if (!strcmp(argv[i], "-t")) text = argv[++i];
else if (!strcmp(argv[i], "-o")) out = argv[++i];
else if (!strcmp(argv[i], "-p")) pitch = atoi(argv[++i]);
else if (!strcmp(argv[i], "-s")) speed = atoi(argv[++i]);
else { usage(); return 2; }
}
if (!voice || !text || !out) { usage(); return 2; }
if (FAILED(CoInitialize(NULL))) {
fprintf(stderr, "CoInitialize failed\n");
return 1;
}
if (FAILED(find_voice(voice, &mode))) {
fprintf(stderr, "voice not found: %s\navailable:\n", voice);
list_voices();
CoUninitialize();
return 1;
}
printf("voice: %s\n", mode.szModeName);
PIAUDIOFILE af = NULL;
HRESULT hr = CoCreateInstance(&CLSID_AudioDestFile, NULL, CLSCTX_ALL,
&IID_IAudioFile, (void **)&af);
if (FAILED(hr) || !af) {
fprintf(stderr, "cannot create audio destination (0x%08lx)\n",
(unsigned long)hr);
CoUninitialize();
return 1;
}
PITTSCENTRAL central = NULL;
{
PITTSFIND f = NULL;
hr = CoCreateInstance(&CLSID_TTSEnumerator, NULL, CLSCTX_ALL,
&IID_ITTSFind, (void **)&f);
if (FAILED(hr) || !f) {
fprintf(stderr, "cannot create TTS find (0x%08lx)\n",
(unsigned long)hr);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
hr = f->lpVtbl->Select(f, mode.gModeID, &central, (LPUNKNOWN)af);
f->lpVtbl->Release(f);
if (FAILED(hr) || !central) {
fprintf(stderr, "voice select failed (0x%08lx)\n",
(unsigned long)hr);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
}
af->lpVtbl->RealTimeSet(af, (WORD)TTSATTR_MAXREALTIME);
WCHAR wout[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, out, -1, wout, MAX_PATH);
hr = af->lpVtbl->Set(af, wout, 1);
if (FAILED(hr)) {
fprintf(stderr, "cannot open output file (0x%08lx)\n",
(unsigned long)hr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
PITTSATTRIBUTES attr = NULL;
hr = central->lpVtbl->QueryInterface(central, &IID_ITTSAttributes,
(void **)&attr);
if (FAILED(hr) || !attr) {
fprintf(stderr, "no ITTSAttributes (0x%08lx)\n", (unsigned long)hr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
WORD pmin = 0, pmax = 0, pdef = 0;
DWORD smin = 0, smax = 0, sdef = 0;
attr->lpVtbl->PitchGet(attr, &pdef);
attr->lpVtbl->SpeedGet(attr, &sdef);
attr->lpVtbl->PitchSet(attr, TTSATTR_MINPITCH);
attr->lpVtbl->PitchGet(attr, &pmin);
attr->lpVtbl->PitchSet(attr, TTSATTR_MAXPITCH);
attr->lpVtbl->PitchGet(attr, &pmax);
attr->lpVtbl->SpeedSet(attr, TTSATTR_MINSPEED);
attr->lpVtbl->SpeedGet(attr, &smin);
attr->lpVtbl->SpeedSet(attr, TTSATTR_MAXSPEED);
attr->lpVtbl->SpeedGet(attr, &smax);
if (pitch < 0) pitch = pdef;
if (speed < 0) speed = sdef;
if (pitch < (int)pmin) pitch = pmin;
if (pitch > (int)pmax) pitch = pmax;
if (speed < (int)smin) speed = smin;
if (speed > (int)smax) speed = smax;
printf("pitch: %d (%d..%d) speed: %d (%lu..%lu)\n",
pitch, pmin, pmax, speed, (unsigned long)smin, (unsigned long)smax);
attr->lpVtbl->PitchSet(attr, (WORD)pitch);
attr->lpVtbl->SpeedSet(attr, (DWORD)speed);
{
DWORD regkey = 0;
hr = central->lpVtbl->Register(central, &g_notify,
IID_ITTSNotifySink, &regkey);
if (FAILED(hr)) {
fprintf(stderr, "Register failed (0x%08lx)\n", (unsigned long)hr);
attr->lpVtbl->Release(attr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
return 1;
}
}
{
SDATA data;
data.dwSize = (DWORD)(strlen(text) + 1);
data.pData = (PVOID)text;
central->lpVtbl->AudioReset(central);
hr = central->lpVtbl->TextData(central, CHARSET_TEXT,
TTSDATAFLAG_TAGGED, data, NULL,
IID_ITTSBufNotifySink);
if (FAILED(hr)) {
fprintf(stderr, "TextData failed (0x%08lx)\n", (unsigned long)hr);
} else {
DWORD start = GetTickCount();
while (!g_done) {
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (!g_started && GetTickCount() - start > 30000) {
fprintf(stderr, "timed out waiting for audio start\n");
break;
}
if (GetTickCount() - start > 120000) {
fprintf(stderr, "timed out waiting for audio stop\n");
break;
}
Sleep(10);
}
}
}
af->lpVtbl->Flush(af);
attr->lpVtbl->Release(attr);
central->lpVtbl->Release(central);
af->lpVtbl->Release(af);
CoUninitialize();
printf("done%s\n", g_done ? "" : " (incomplete)");
return g_done ? 0 : 1;
}
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Convert INF AddReg sections into a Windows/Wine .reg file.
Example:
inf2reg.py --section APIReg --define 49000='C:\\windows\\speech' \
spchapi/SPCHAPI.INF > spchapi.reg
"""
import argparse
import re
import sys
ROOTS = {
"HKCR": "HKEY_CLASSES_ROOT",
"HKLM": "HKEY_LOCAL_MACHINE",
"HKCU": "HKEY_CURRENT_USER",
"HKU": "HKEY_USERS",
}
def expand(text, defines):
def repl(m):
key = m.group(1)
if key == "25":
return defines.get("WINDOWS", r"C:\windows")
return defines.get(key, m.group(0))
return re.sub(r"%(\w+)%", repl, text)
def reg_type(flags, data):
if not flags:
return "REG_SZ"
try:
f = int(flags, 16) if flags.lower().startswith("0x") else int(flags)
except ValueError:
return "REG_SZ"
if f & 0x00020000:
return "REG_EXPAND_SZ"
if f & 0x00010000:
return "REG_MULTI_SZ"
if f & 0x00000001:
return "REG_BINARY"
return "REG_SZ"
def reg_escape(value):
return value.replace("\\", "\\\\").replace('"', '\\"')
def unquote(token):
token = token.strip()
if len(token) >= 2 and token[0] == '"' and token[-1] == '"':
return token[1:-1]
return token
def main():
ap = argparse.ArgumentParser()
ap.add_argument("inf")
ap.add_argument("--section", action="append", required=True,
help="AddReg section name (repeatable)")
ap.add_argument("--define", action="append", default=[],
help="ID=value, e.g. 49000=C:\\windows\\speech")
args = ap.parse_args()
defines = {"WINDOWS": r"C:\windows"}
for item in args.define:
k, v = item.split("=", 1)
defines[k] = v
wanted = set(args.section)
section = None
out = ["REGEDIT4", ""]
with open(args.inf, "r", encoding="latin-1") as fh:
for raw in fh:
line = raw.strip()
if not line or line.startswith(";"):
continue
m = re.match(r"^\[([^\]]+)\]$", line)
if m:
section = m.group(1)
continue
if section not in wanted:
continue
parts = [p.strip() for p in line.split(",")]
if len(parts) < 4:
continue
root, key, name, flags = parts[0], parts[1], parts[2], parts[3]
data = ",".join(parts[4:]) if len(parts) > 4 else ""
if root.upper() not in ROOTS:
continue
data = expand(unquote(data), defines)
name = expand(unquote(name), defines)
if "%InstallPrompt%" in name:
continue
if not data:
continue
rtype = reg_type(flags.strip(), data)
keypath = "\\".join(
p for p in (ROOTS[root.upper()], expand(key, defines)) if p
)
value = data if rtype == "REG_BINARY" else '"%s"' % reg_escape(data)
out.append("[%s]" % keypath)
out.append("%s=%s" % ('@' if not name else '"%s"' % name, value))
out.append("")
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
main()
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# Install Wine + 32-bit Winelib toolchain + helpers. Debian/Ubuntu.
# Run as root: sudo setup/install-deps.sh
set -eu
[ "$(id -u)" = "0" ] || { echo "run as root: sudo $0" >&2; exit 1; }
export DEBIAN_FRONTEND=noninteractive
if ! dpkg --print-foreign-architectures | grep -qx i386; then
echo "== enabling i386 architecture"
dpkg --add-architecture i386
fi
apt-get update
for p in \
wine wine64 wine32:i386 \
wine64-tools \
libwine-dev libwine-dev:i386 \
gcc-multilib libc6-dev-i386 \
mingw-w64 \
xvfb unshield
do
if apt-cache policy "$p" 2>/dev/null | grep -q "Candidate: [0-9]"; then
echo "== install $p"
apt-get install -y --no-install-recommends "$p" || echo "!! FAILED: $p"
else
echo "-- unavailable, skipped: $p"
fi
done
echo
echo "versions:"
wine --version 2>/dev/null || echo "wine missing"
winegcc --version 2>/dev/null | head -1 || echo "winegcc missing"
i686-w64-mingw32-gcc --version 2>/dev/null | head -1 || echo "mingw missing"
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# Run the SAPI4 host with the project's wine prefix and a working X display.
# Usage: setup/run.sh [sapi4host args...]
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
export WINEARCH=win32
if [ -z "${DISPLAY:-}" ]; then
if ! [ -S /tmp/.X11-unix/X77 ]; then
Xvfb :77 -screen 0 1024x768x24 >/dev/null 2>&1 &
sleep 1
fi
export DISPLAY=:77
fi
HOST="$ROOT/build/sapi4host"
if [ ! -x "$HOST" ]; then
HOST="$ROOT/build/sapi4host.exe"
fi
if [ ! -x "$HOST" ]; then
echo "no host binary; build first (see README)" >&2
exit 1
fi
exec "$HOST" "$@"
+95
View File
@@ -0,0 +1,95 @@
#!/bin/sh
# Install AT&T FlexTalk 2.1 (Keith Bell, Kerry Watson) into the wine prefix.
#
# Source: extracted InstallShield cabinet, e.g.
# flextalk/extracted/TTS_Executables
# Usage: setup/setup-flextalk.sh [source-dir]
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
SRC=${1:-$ROOT/../flextalk/extracted/TTS_Executables}
[ -d "$SRC/EngineDLLs" ] || { echo "source not found: $SRC" >&2; exit 1; }
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
export WINEARCH=win32
if [ -z "${DISPLAY:-}" ]; then
WRUN="xvfb-run -a wine"
else
WRUN="wine"
fi
DEST="$WINEPREFIX/drive_c/FlexTalk"
echo "== files -> $DEST"
mkdir -p "$DEST"
cp -a "$SRC/." "$DEST/"
REG="$HERE/out-flextalk.reg"
cat > "$REG" <<'EOF'
REGEDIT4
[HKEY_LOCAL_MACHINE\Software\Voice\TextToSpeech\Engine]
"AT&T FlexTalk"="{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}"
[HKEY_LOCAL_MACHINE\Software\Voice\TextToSpeech\Engine\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}]
"Dirty"=dword:00000001
[HKEY_CLASSES_ROOT\CLSID\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}]
@="AT&T FlexTalk"
[HKEY_CLASSES_ROOT\CLSID\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}\InprocServer32]
@="C:\\FlexTalk\\EngineDLLs\\FlexTalk.dll"
"ThreadingModel"="Apartment"
[HKEY_CLASSES_ROOT\CLSID\{778EAAE2-499F-11CF-B0BA-444553540000}\InprocServer32]
@="C:\\FlexTalk\\EngineDLLs\\FlexTalk.dll"
"ThreadingModel"="Apartment"
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Directories]
"RootDirectory"="C:\\FlexTalk\\"
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Dictionaries]
"StandardDictionary"="2 ft_sdict.dll"
"ExceptionDictionary"="0 ft_udict.dll"
"HomographDictionary"="1 ft_hgrap.dll"
"ProperNameDictionary"="3 ft_namsa.dll"
"LetterToSoundDictionary"="4 ft_l2s.dll"
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Options]
"Gender"="Male"
"SampleRate"="11025"
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Speakers\{11260610-C84A-11ce-AEC4-0000C0E9CB87}]
"Age"="30"
"Expression"="59"
"Gender"=""
"Head Size"="46"
"Lowering"="40"
"Mode Name"="Keith Bell"
"Pitch"="27"
"Prominences"="17"
"Speaker Name"="Keith Bell"
"Speaking Rate"="33"
"Volume"="100"
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Speakers\{000031CB-3699-551F-AED0-6B4893382D57}]
"Age"="30"
"Expression"="100"
"Gender"=""
"Head Size"="45"
"Lowering"="40"
"Mode Name"="Kerry Watson"
"Pitch"="65"
"Prominences"="17"
"Speaker Name"="Kerry Watson"
"Speaking Rate"="32"
"Volume"="100"
EOF
echo "== registry"
$WRUN regedit /S "$REG" 2>/dev/null || true
echo "== done; list voices:"
echo " $WRUN $ROOT/host/sapi4host.exe -l"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# Build a 32-bit Wine prefix, install the SAPI4 runtime, the MSTTS engine and
# the MSTTS voices, and register everything.
#
# Usage: setup/setup-prefix.sh [path-to-asset-repo]
# Default asset repo: ../Microsoft-Sam-Mary-Mike-TruVoice-WSAPI4
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
ASSETS=${1:-$ROOT/../Microsoft-Sam-Mary-Mike-TruVoice-WSAPI4}
[ -d "$ASSETS/spchapi" ] || { echo "asset dir not found: $ASSETS" >&2; exit 1; }
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
export WINEARCH=win32
if [ -z "${DISPLAY:-}" ]; then
WRUN="xvfb-run -a wine"
else
WRUN="wine"
fi
echo "== prefix: $WINEPREFIX"
mkdir -p "$WINEPREFIX"
$WRUN wineboot -u
SPEECH_DIR="$WINEPREFIX/drive_c/windows/speech"
ENGINE_DIR="$WINEPREFIX/drive_c/Program Files/Common Files/microsoft shared/SpeechEngines/TTS"
mkdir -p "$SPEECH_DIR" "$ENGINE_DIR"
echo "== runtime -> $SPEECH_DIR"
cp "$ASSETS"/spchapi/SPEECH.DLL "$SPEECH_DIR/"
echo "== engine + voices -> $ENGINE_DIR"
cp "$ASSETS"/msttsl/msttssyn.dll "$ENGINE_DIR/"
cp "$ASSETS"/msttsl/*.vce "$ASSETS"/msttsl/*.cfg "$ENGINE_DIR/"
echo "== registry"
python3 "$HERE/inf2reg.py" --section APIReg \
--define "49000=C:\\windows\\speech" \
"$ASSETS/spchapi/SPCHAPI.INF" > "$HERE/out-spchapi.reg"
python3 "$HERE/inf2reg.py" --section TTSReg \
--define "49000=C:\\Program Files\\Common Files\\microsoft shared\\SpeechEngines\\TTS" \
"$ASSETS/msttsl/mstts.inf" > "$HERE/out-mstts.reg"
$WRUN regedit /S "$HERE/out-spchapi.reg"
$WRUN regedit /S "$HERE/out-mstts.reg"
echo "== done"
echo "list voices: $WRUN $ROOT/host/sapi4host.exe -l"