Winelib SAPI4 host: MSTTS (Sam/Mary/Mike) + AT&T Flextalk (Keith Bell/Kerry Watson)
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
build/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
setup/out-*.reg
|
||||||
|
*.wav
|
||||||
@@ -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
|
||||||
@@ -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).
|
||||||
@@ -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)
|
||||||
@@ -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, ¢ral, (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, ®key);
|
||||||
|
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;
|
||||||
|
}
|
||||||
Executable
+112
@@ -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()
|
||||||
Executable
+37
@@ -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
@@ -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" "$@"
|
||||||
Executable
+95
@@ -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"
|
||||||
Executable
+51
@@ -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"
|
||||||
Reference in New Issue
Block a user