140 lines
7.9 KiB
Markdown
140 lines
7.9 KiB
Markdown
# 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). **All numeric
|
|
values must be REG_DWORD**: with REG_SZ the engine reads garbage and
|
|
synthesizes a monotone buzz (pitch fixed at max, pitch control dead).
|
|
- `{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 package is single-volume: the original 1997 upload log
|
|
(`WS_FTP.LOG`) lists `data1.cab` only, and `-O` extracts all 450 entries with exact
|
|
sizes. Without `-O`, unshield's new decompressor reports a phantom "volume 2" and fails
|
|
on `MSVCRT.DLL` / `WatsonUNInstall.dll`; with `-O` both are complete, valid PE files.
|
|
|
|
The `.ENV` files the engine probes (`ae1m11lc.ENV`) were never shipped; synthesis works
|
|
without them.
|
|
|
|
## Latency and output shape (measured 2026-09-16)
|
|
|
|
- Warm wrapper time: ~410-425 ms per short line (engine call 190-220 ms, ffmpeg measure
|
|
~110 ms, apply ~90 ms, profile write ~57 ms when it runs). First (cold) call ~730 ms.
|
|
- The Flextalk profile write is cached per voice in the Wine prefix
|
|
(`~/.wine-microslopsam/.microslopsam-profile<GUID>.reg`): unchanged profiles skip
|
|
`wine reg import`. Recreating the prefix invalidates the cache.
|
|
- Fades: the 8 ms fade-in/out is applied to the trimmed file whose 25-30 ms padding
|
|
silence is retained, so it shapes silence, not speech. Measured head: first sample
|
|
>30 at 22.6 ms, speech (>500) at 29.6 ms, natural ~15 ms attack; tail decays over
|
|
~18 ms. No click, no attenuation of the first word.
|
|
- Leading hum (default on): 80 ms of 75 Hz at -48 dBFS peak before the engine padding,
|
|
to prime Discord's pipeline. Verified 75.0 Hz / -48.0 dBFS, speech onset ~110 ms,
|
|
final -18.6 LUFS / -3.0 dBFS, apply pass +~10 ms. `VESKSLOP_TTS_HUM_MS=0` disables and
|
|
reproduces the previous output byte-for-byte. Tuning: `_HUM_HZ` (30-200), `_HUM_DB`
|
|
(-80..-6; raise toward -36 if a client still gates the start).
|
|
The hum bypasses the high-pass/trim chain, so `VESKSLOP_TTS_HIGHPASS` (up to 400 Hz)
|
|
cannot remove it (verified at 200 and 400 Hz).
|
|
|
|
## 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).
|
|
- Generate registry `.reg` files with `printf`, never `echo`: dash's `echo`
|
|
interprets `\2` in `...\Watson\2.0\...` as 0x02, corrupting the key so that
|
|
`wine reg import` is a silent no-op (it still returns 0). The wrapper rejects
|
|
a generated profile containing control characters.
|
|
- 32-bit engines require a 32-bit prefix (`WINEARCH=win32`).
|
|
- `sudo` on this machine requires a password; installs are manual.
|
|
|
|
## Engine units (measured)
|
|
|
|
- speed: MSTTS range 30-450, Flextalk 50-390; the natural rate is about 150 for every
|
|
voice (duration at 100/200/300 fits a natural value near 140-150).
|
|
- pitch: engine units, roughly linear in F0 near the natural point (~1.1 Hz per unit).
|
|
Natural values: about 100 (MSTTS), 106 (Keith Bell), 186 (Kerry Watson); Flextalk max 250.
|
|
Verified with the corrected DWORD registry: Kerry 0 st = 209 Hz, -6 st = 148 Hz,
|
|
+6 st = 250 Hz (clamped at max); Keith 0 st = 119 Hz, +6 st = 170 Hz.
|
|
- The wrapper maps semitones as `natural * 2^(st/12)` and clamps to the range.
|
|
- volume: available through `ITTSAttributes` (host flag `-V`). Flextalk is linear
|
|
(0-65535, default 65535; half is about -6 dB). MSTTS is non-linear (0 = silence,
|
|
4096 about -24 dB, 65535+ full scale); treat it as on/off.
|
|
- Flextalk per-speaker profile values are applied at voice load; `Expression` was
|
|
verified audible (0/59/100 produce different waveforms at equal duration). Changing
|
|
them requires a registry edit and re-selecting the voice.
|
|
|
|
## VOX parser
|
|
|
|
- `setup/vox_parser.py` reimplements the NetTTS VOX prosody encoder (vendor tags
|
|
`\!wH1/\!wH0`, `\!br`, `\!sfNN`); the host already passes `TTSDATAFLAG_TAGGED`, so
|
|
the engine honors them.
|
|
- Validation: the reference `vox_parser.cpp` from a local clone of
|
|
`h4rm0n1c/NetTTS` was compiled with g++ in a throwaway harness and diffed against
|
|
the Python port; outputs are identical on the 31-line corpus. Re-check after any
|
|
parser change while the clone is available.
|
|
- VOX mode is Flextalk-only. The wrapper enables it with `VESKSLOP_TTS_VOX=1`
|
|
(`VESKSLOP_TTS_VOX_CLEAN=1` drops the stutter wrapper) and it takes precedence
|
|
over `VESKSLOP_TTS_WORDS`.
|
|
- Line-leading `/rate N` and `/pitch N` controls are consumed by the wrapper
|
|
(percent, 100 = normal; `<= 2.0` means multiplier). They override the env
|
|
speed/pitch and are not spoken; a controls-only line yields 100 ms of silence.
|
|
|
|
## Decompiled corpora (Ghidra)
|
|
|
|
- `/mnt/ghidra/decomp/msttssyn.dll/` (786 functions, engine)
|
|
- `/mnt/ghidra/decomp/speech.dll/` (1030 functions, SAPI4 runtime)
|