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

9.0 KiB

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)