VOX: apply rate/pitch as vendor tags (NetTTS-style); full profile in both hives; docs
This commit is contained in:
@@ -134,6 +134,11 @@ without them.
|
|||||||
head size 52, volume 70 (factory: 27/33/17/46/100); `vox` also implies VOX prosody.
|
head size 52, volume 70 (factory: 27/33/17/46/100); `vox` also implies VOX prosody.
|
||||||
`hev` (pitch 15, rate 24) is an unverified starting point - HL1 credits the suit
|
`hev` (pitch 15, rate 24) is an unverified starting point - HL1 credits the suit
|
||||||
sounds to Kelly Bailey with no published settings.
|
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
|
- Line-leading `/rate N` and `/pitch N` controls are consumed by the wrapper
|
||||||
(percent, 100 = normal; `<= 2.0` means multiplier). They override the env
|
(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.
|
speed/pitch and are not spoken; a controls-only line yields 100 ms of silence.
|
||||||
|
|||||||
+63
-7
@@ -412,14 +412,68 @@ Kerry Watson at factory values. The wrapper implements it as a preset:
|
|||||||
- Precedence: preset < `VESKSLOP_TTS_PROFILE` < `VESKSLOP_TTS_EXPRESSION`.
|
- Precedence: preset < `VESKSLOP_TTS_PROFILE` < `VESKSLOP_TTS_EXPRESSION`.
|
||||||
- Equivalent explicit override:
|
- Equivalent explicit override:
|
||||||
`VESKSLOP_TTS_PROFILE='pitch:3,rate:17,volume:70,prominences:22,head_size:52,lowering:40'`.
|
`VESKSLOP_TTS_PROFILE='pitch:3,rate:17,volume:70,prominences:22,head_size:52,lowering:40'`.
|
||||||
- The NetTTS profile also sets `Gender`, `Line Mode`, and a set of text-class DWORDs
|
|
||||||
(address/date/money classes, abbreviation expansion, and so on). Those affect text
|
|
||||||
normalization rather than the audible voice, so the wrapper does not write them.
|
|
||||||
- HEV suit: Half-Life 1 credits the suit sounds to Kelly Bailey; no settings were ever
|
- HEV suit: Half-Life 1 credits the suit sounds to Kelly Bailey; no settings were ever
|
||||||
published. The `hev` preset is a starting point (less extreme than VOX); tune pitch and
|
published. The `hev` preset is a starting point (less extreme than VOX); tune pitch and
|
||||||
rate to taste. VOX prosody is optional there (`VESKSLOP_TTS_VOX=1`).
|
rate to taste. VOX prosody is optional there (`VESKSLOP_TTS_VOX=1`).
|
||||||
- Measured with the wrapper on the same line: VOX preset 5.83 s, HEV 3.79 s, factory
|
|
||||||
3.35 s (the VOX preset is slower and gains the VOX cadence).
|
#### How NetTTS applies the settings (reference behaviour)
|
||||||
|
|
||||||
|
Checked against the NetTTS sources (`src/main.cpp`, `src/tts_engine.cpp`,
|
||||||
|
`scripts/winetricks/nettts_flextalk_vox_profile.reg`):
|
||||||
|
|
||||||
|
1. **Voice**: the imported profile points `Default Speaker` at Keith Bell's GUID, and
|
||||||
|
NetTTS selects the default mode (empty `TTSMODEINFO`) rather than a voice name.
|
||||||
|
2. **Speaker profile**: the full key set is imported into **both** hives
|
||||||
|
(`HKEY_CURRENT_USER` and `HKEY_LOCAL_MACHINE`) at prefix setup; every numeric value is
|
||||||
|
`REG_DWORD`.
|
||||||
|
3. **Rate and pitch are never set through `ITTSAttributes`.** NetTTS keeps UI percentages
|
||||||
|
and prepends sticky vendor tags to the utterance text:
|
||||||
|
- `\!R<factor>` = duration factor. Smaller is faster: `0.30` very fast, `1.00` 1.0x,
|
||||||
|
`1.60` slow. No tag at 100%.
|
||||||
|
- `\!%<factor>` = pitch multiplier. `0.85` lower, `1.20` higher. No tag at 100%.
|
||||||
|
4. **Volume** goes through `ITTSAttributes::VolumeSet(percent * 65535 / 100)` (Flextalk
|
||||||
|
scale; MSTTS uses 0..0xFFFFFFFF).
|
||||||
|
5. **Prosody**: the VOX parser output wrapped in `\!wH1 ... \!wH0`.
|
||||||
|
6. The tags are sticky inside an engine instance; NetTTS sends the neutral tag only to
|
||||||
|
reset. Our wrapper starts a fresh engine per line, so stickiness cannot leak.
|
||||||
|
|
||||||
|
#### How to implement it correctly (and the bug this fixed)
|
||||||
|
|
||||||
|
Earlier wrapper versions mapped `VESKSLOP_TTS_SPEED`/`_PITCH` to `ITTSAttributes`
|
||||||
|
`-s`/`-p`. Measured with the VOX profile loaded, Veskslop's neutral slider values
|
||||||
|
(`speed=1.0, pitch=0`) sent `-s 150 -p 106`, which overrode the registry profile: F0
|
||||||
|
jumped from 87 Hz back to 120 Hz and the line shortened from 5.83 s to 5.10 s. That is
|
||||||
|
the "too fast, not deep enough" report.
|
||||||
|
|
||||||
|
The correct implementation for Flextalk:
|
||||||
|
|
||||||
|
1. Write the speaker profile first (both hives, `REG_DWORD`, full key set for presets;
|
||||||
|
`Default Speaker` for presets).
|
||||||
|
2. Translate rate/pitch to vendor tags, **not** attributes:
|
||||||
|
- speed multiplier `m` -> `\!R(1/m)` (clamp 0.25-4.0)
|
||||||
|
- pitch semitones `st` -> `\!%(2^(st/12))` (clamp 0.5-2.0)
|
||||||
|
3. Emit **no tag when neutral** (`m == 1`, `st == 0`). `\!R1.00` is a literal 1.0x rate,
|
||||||
|
not the profile rate, so emitting it defeats the preset.
|
||||||
|
4. Prepend the tags to the text (`\!R0.77 \!%1.19 Attention all personnel.`); for word
|
||||||
|
mode, prepend to each word because every word is a separate engine instance.
|
||||||
|
5. Keep `-s`/`-p` only for MSTTS voices, which have no vendor tags.
|
||||||
|
6. Verify with F0 and duration, not by ear: factory Keith ~3.35 s / ~123 Hz; VOX preset
|
||||||
|
~5.83 s / ~87 Hz; neutral sliders must reproduce the preset byte-for-byte.
|
||||||
|
|
||||||
|
Verified matrix (same line, Keith Bell):
|
||||||
|
|
||||||
|
| Run | Duration | F0 median |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| factory, no adjustments | 3.35 s | 123 Hz |
|
||||||
|
| VOX preset, no adjustments | 5.83 s | 87 Hz |
|
||||||
|
| VOX preset + `speed=1.0 pitch=0` | 5.83 s, byte-identical | 87 Hz |
|
||||||
|
| VOX preset + `speed=1.3` | 4.02 s | 84 Hz |
|
||||||
|
| VOX preset + `pitch=+3` | 5.83 s | 88 Hz |
|
||||||
|
| HEV preset, no adjustments | 3.79 s | - |
|
||||||
|
|
||||||
|
Registry inspection after a preset run: 26 values in the Keith speaker key in both
|
||||||
|
hives, `Pitch=0x3`, `Speaking Rate=0x11`, `Gender=0x2`, `Line Mode=0x1`,
|
||||||
|
`Default Speaker={11260610-C84A-11CE-AEC4-0000C0E9CB87}`.
|
||||||
|
|
||||||
### Inline rate/pitch controls (engine-side)
|
### Inline rate/pitch controls (engine-side)
|
||||||
|
|
||||||
@@ -487,8 +541,10 @@ This tree is written to survive a copy into the Veskslop repository next to its
|
|||||||
(`VESKSLOP_TTS_*`, `{out}`, `{text_file}`, voice/speed/pitch/expression/profile/
|
(`VESKSLOP_TTS_*`, `{out}`, `{text_file}`, voice/speed/pitch/expression/profile/
|
||||||
highpass/echo/words/word_gap_ms). New features are additive and default off.
|
highpass/echo/words/word_gap_ms). New features are additive and default off.
|
||||||
3. Optional Veskslop-side follow-up: a `tts.vox` preference that exports
|
3. Optional Veskslop-side follow-up: a `tts.vox` preference that exports
|
||||||
`VESKSLOP_TTS_VOX` (like `words`) and a line in the TTS panel help. The inline
|
`VESKSLOP_TTS_VOX` (like `words`) and a line in the TTS panel help. If presets are
|
||||||
`/rate` and `/pitch` controls need no UI work at all.
|
exposed, also export `VESKSLOP_TTS_PRESET` and have the voice metadata defaults reflect
|
||||||
|
the preset values so the UI's "default" snap points match. The inline `/rate` and
|
||||||
|
`/pitch` controls need no UI work at all.
|
||||||
|
|
||||||
### Transport and latency
|
### Transport and latency
|
||||||
|
|
||||||
|
|||||||
+80
-23
@@ -159,19 +159,38 @@ if [ -f "$TEXTFILE" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build the engine arguments from the (possibly command-overridden) speed/pitch.
|
# Build the engine controls from the (possibly command-overridden) speed/pitch.
|
||||||
|
# Flextalk voices take sticky vendor tags relative to the registry profile, the
|
||||||
|
# way NetTTS does it: \!R is a duration factor (smaller = faster, 1.0 neutral)
|
||||||
|
# and \!% is a pitch multiplier (1.0 neutral). ITTSAttributes -s/-p would
|
||||||
|
# override the profile and make presets sound fast and shallow, so tags are used
|
||||||
|
# for Flextalk. MSTTS has no vendor tags and keeps -s/-p.
|
||||||
SET_ARGS=""
|
SET_ARGS=""
|
||||||
if [ -n "$SPEED" ]; then
|
TAG_PREFIX=""
|
||||||
S=$(awk -v m="$SPEED" 'BEGIN{printf "%d", 150*m+0.5}')
|
if [ -n "$SPEAKER_GUID" ]; then
|
||||||
[ "$S" -lt 50 ] && S=50
|
if [ -n "$SPEED" ]; then
|
||||||
[ "$S" -gt "$SMAX" ] && S=$SMAX
|
# A neutral multiplier must not emit a tag: \!R1.00 means a 1.0x rate,
|
||||||
SET_ARGS="$SET_ARGS -s $S"
|
# which would override the profile's own speaking rate.
|
||||||
fi
|
R=$(awk -v m="$SPEED" 'BEGIN{ if (m<=0) m=1; if (m==1) { exit } f=1/m; if (f<0.25) f=0.25; if (f>4) f=4; printf "%.2f", f }')
|
||||||
if [ -n "$PITCH" ]; then
|
[ -n "$R" ] && TAG_PREFIX="$TAG_PREFIX\\!R$R "
|
||||||
P=$(awk -v st="$PITCH" -v nat="$NAT_PITCH" 'BEGIN{printf "%d", nat*exp(log(2)*st/12)+0.5}')
|
fi
|
||||||
[ "$P" -lt 50 ] && P=50
|
if [ -n "$PITCH" ]; then
|
||||||
[ "$P" -gt "$PMAX" ] && P=$PMAX
|
PCT=$(awk -v st="$PITCH" 'BEGIN{ if (st==0) { exit } f=exp(log(2)*st/12); if (f<0.5) f=0.5; if (f>2) f=2; printf "%.2f", f }')
|
||||||
SET_ARGS="$SET_ARGS -p $P"
|
[ -n "$PCT" ] && TAG_PREFIX="$TAG_PREFIX\\!%$PCT "
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ -n "$SPEED" ]; then
|
||||||
|
S=$(awk -v m="$SPEED" 'BEGIN{printf "%d", 150*m+0.5}')
|
||||||
|
[ "$S" -lt 50 ] && S=50
|
||||||
|
[ "$S" -gt "$SMAX" ] && S=$SMAX
|
||||||
|
SET_ARGS="$SET_ARGS -s $S"
|
||||||
|
fi
|
||||||
|
if [ -n "$PITCH" ]; then
|
||||||
|
P=$(awk -v st="$PITCH" -v nat="$NAT_PITCH" 'BEGIN{printf "%d", nat*exp(log(2)*st/12)+0.5}')
|
||||||
|
[ "$P" -lt 50 ] && P=50
|
||||||
|
[ "$P" -gt "$PMAX" ] && P=$PMAX
|
||||||
|
SET_ARGS="$SET_ARGS -p $P"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply the Flextalk profile before synthesis. Profiles are read at voice load and
|
# Apply the Flextalk profile before synthesis. Profiles are read at voice load and
|
||||||
@@ -244,19 +263,57 @@ if [ -n "$SPEAKER_GUID" ]; then
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
REG_KEY="HKLM\\Software\\AT&T\\Watson\\2.0\\FlexTalk\\2.0\\Speakers\\$SPEAKER_GUID"
|
# NetTTS's VOX profile also carries Gender, Line Mode, the text-class
|
||||||
|
# switches and the default-speaker pointer. Write the full set for presets,
|
||||||
|
# exactly like its nettts_flextalk_vox_profile.reg, in both hives.
|
||||||
|
FULL_PROFILE=0
|
||||||
|
if [ "$PRESET" = "vox" ] || [ "$PRESET" = "hev" ]; then
|
||||||
|
if [ "$ENGVOICE" = "Keith Bell" ]; then FULL_PROFILE=1; fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
REG_ROOT='Software\AT&T\Watson\2.0\FlexTalk\2.0'
|
||||||
# Use printf, not echo: dash's echo interprets \2 in the key path as an
|
# Use printf, not echo: dash's echo interprets \2 in the key path as an
|
||||||
# escape (STX), which corrupts the key and makes the import a silent no-op.
|
# escape (STX), which corrupts the key and makes the import a silent no-op.
|
||||||
{
|
{
|
||||||
printf 'Windows Registry Editor Version 5.00\n\n'
|
printf 'Windows Registry Editor Version 5.00\n\n'
|
||||||
printf '[%s]\n' "$REG_KEY"
|
if [ "$FULL_PROFILE" = "1" ]; then
|
||||||
printf '"Pitch"=dword:%08x\n' "$P_PITCH"
|
printf '[HKEY_LOCAL_MACHINE\\%s]\n' "$REG_ROOT"
|
||||||
printf '"Speaking Rate"=dword:%08x\n' "$P_RATE"
|
printf '"Default Speaker"="%s"\n\n' "$SPEAKER_GUID"
|
||||||
printf '"Volume"=dword:%08x\n' "$P_VOLUME"
|
printf '[HKEY_CURRENT_USER\\%s]\n' "$REG_ROOT"
|
||||||
printf '"Expression"=dword:%08x\n' "$P_EXPR"
|
printf '"Default Speaker"="%s"\n\n' "$SPEAKER_GUID"
|
||||||
printf '"Prominences"=dword:%08x\n' "$P_PROM"
|
fi
|
||||||
printf '"Head Size"=dword:%08x\n' "$P_HEAD"
|
for hive in HKEY_LOCAL_MACHINE HKEY_CURRENT_USER; do
|
||||||
printf '"Lowering"=dword:%08x\n' "$P_LOWER"
|
printf '[%s\\%s\\Speakers\\%s]\n' "$hive" "$REG_ROOT" "$SPEAKER_GUID"
|
||||||
|
printf '"Pitch"=dword:%08x\n' "$P_PITCH"
|
||||||
|
printf '"Speaking Rate"=dword:%08x\n' "$P_RATE"
|
||||||
|
printf '"Volume"=dword:%08x\n' "$P_VOLUME"
|
||||||
|
printf '"Expression"=dword:%08x\n' "$P_EXPR"
|
||||||
|
printf '"Prominences"=dword:%08x\n' "$P_PROM"
|
||||||
|
printf '"Head Size"=dword:%08x\n' "$P_HEAD"
|
||||||
|
printf '"Lowering"=dword:%08x\n' "$P_LOWER"
|
||||||
|
if [ "$FULL_PROFILE" = "1" ]; then
|
||||||
|
printf '"Abbreviation Expansion"=dword:00000001\n'
|
||||||
|
printf '"Address Class"=dword:00000001\n'
|
||||||
|
printf '"Age"=dword:0000001e\n'
|
||||||
|
printf '"Alphabetic Hyphen"=dword:00000000\n'
|
||||||
|
printf '"Case Significance"=dword:00000001\n'
|
||||||
|
printf '"Date Class"=dword:00000001\n'
|
||||||
|
printf '"Fraction Class"=dword:00000001\n'
|
||||||
|
printf '"Gender"=dword:00000002\n'
|
||||||
|
printf '"Line Mode"=dword:00000001\n'
|
||||||
|
printf '"Math Mode"=dword:00000000\n'
|
||||||
|
printf '"Measurements Class"=dword:00000001\n'
|
||||||
|
printf '"Money Class"=dword:00000001\n'
|
||||||
|
printf '"Numerical Hyphen"=dword:00000000\n'
|
||||||
|
printf '"Ordinal Numbers Class"=dword:00000001\n'
|
||||||
|
printf '"ProofRead Mode"=dword:00000000\n'
|
||||||
|
printf '"Proper Names Class"=dword:00000001\n'
|
||||||
|
printf '"Spell Mode"=dword:00000000\n'
|
||||||
|
printf '"Telephone Numbers Class"=dword:00000001\n'
|
||||||
|
printf '"Time Class"=dword:00000001\n'
|
||||||
|
fi
|
||||||
|
printf '\n'
|
||||||
|
done
|
||||||
} > "$TMP/profile.reg"
|
} > "$TMP/profile.reg"
|
||||||
|
|
||||||
# Guard against silent reg-import failures (a malformed key would corrupt
|
# Guard against silent reg-import failures (a malformed key would corrupt
|
||||||
@@ -352,7 +409,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$VOX" != "1" ]; then
|
|||||||
INDEX=0
|
INDEX=0
|
||||||
for word in $(cat "$TXT"); do
|
for word in $(cat "$TXT"); do
|
||||||
INDEX=$((INDEX + 1))
|
INDEX=$((INDEX + 1))
|
||||||
try_run_host "$TMP/w$INDEX.wav" "$word"
|
try_run_host "$TMP/w$INDEX.wav" "$TAG_PREFIX$word"
|
||||||
echo "file '$TMP/w$INDEX.wav'" >> "$TMP/list.txt"
|
echo "file '$TMP/w$INDEX.wav'" >> "$TMP/list.txt"
|
||||||
|
|
||||||
if [ "$GAP" -gt 0 ]; then
|
if [ "$GAP" -gt 0 ]; then
|
||||||
@@ -370,7 +427,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$VOX" != "1" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
try_run_host "$RAW" "$(cat "$TXT")"
|
try_run_host "$RAW" "$TAG_PREFIX$(cat "$TXT")"
|
||||||
fi
|
fi
|
||||||
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }
|
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user