Files
MicroslopSam/docs/NOTES.md
T
Fisto de4631fe54 Sync wrapper with Veskslop: streaming/crunch modes, vox/hev voice IDs
The Veskslop integration copy had grown word streaming
(VESKSLOP_TTS_STREAM, stream gain), 1990s crunch filters
(VESKSLOP_TTS_CRUNCH[_BITS|_RATE]), echo timing/level, and first-class
vox/hev voice IDs with preset-aware default metadata. Merge those back
into the standalone repo, keeping the tuned VOX preset (pitch 0, rate 15,
head size 34, lowering 50) in the preset and in the published defaults.

Update the wrapper regression test to the tuned registry values, add
__pycache__ to .gitignore, and document the new variables, voice IDs,
placeholder set and the word-mode-over-VOX precedence in README, NOTES
and TTS-DESIGN.
2026-09-22 13:55:28 +02:00

154 lines
9.0 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); word mode
(`VESKSLOP_TTS_WORDS=1`) wins and skips VOX prosody.
- Half-Life presets (`VESKSLOP_TTS_PRESET=vox|hev`) target Keith Bell only. VOX starts
from NetTTS `nettts_flextalk_vox_profile.reg` (pitch 3, rate 17, head 52, lowering 40)
and is tuned against the game's word assets from `suchipi/half-life-vox-console`:
shipped values are pitch 0, rate 15, prominences 22, head size 34, lowering 50,
volume 70. Measured on 38 words vs the game assets: F0 82.3 vs 82.3 Hz, duration
22.99 vs 23.36 s, >2 kHz share 0.0172 vs 0.0186 (old approximation: 87.5 Hz /
22.28 s / 0.0137). `tools/vox_compare.py` reproduces the comparison. `hev`
(pitch 15, rate 24) remains an unverified starting point - HL1 credits the suit
sounds to Kelly Bailey with no published settings.
- Flextalk rate/pitch are applied as **vendor tags** (`\!R` = duration factor,
`\!%` = pitch multiplier), never `-s`/`-p`, because ITTSAttributes override the
registry profile. Neutral values emit no tag, so Veskslop's default sliders
(1.0 / 0) reproduce the preset byte-for-byte. Presets write the full 26-value NetTTS
profile plus `Default Speaker` into both HKCU and HKLM.
- 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)