Fix Flextalk buzz: REG_DWORD speaker settings; measured pitch/speed mapping; fixed-gain normalization

This commit is contained in:
Fisto
2026-09-15 13:14:50 +02:00
parent fa4fbaf74b
commit ad81303b6f
5 changed files with 105 additions and 54 deletions
+16 -16
View File
@@ -62,30 +62,30 @@ REGEDIT4
"SampleRate"="11025"
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Speakers\{11260610-C84A-11ce-AEC4-0000C0E9CB87}]
"Age"="30"
"Expression"="59"
"Age"=dword:0000001e
"Expression"=dword:0000003b
"Gender"=""
"Head Size"="46"
"Lowering"="40"
"Head Size"=dword:0000002e
"Lowering"=dword:00000028
"Mode Name"="Keith Bell"
"Pitch"="27"
"Prominences"="17"
"Pitch"=dword:0000001b
"Prominences"=dword:00000011
"Speaker Name"="Keith Bell"
"Speaking Rate"="33"
"Volume"="100"
"Speaking Rate"=dword:00000021
"Volume"=dword:00000064
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Speakers\{000031CB-3699-551F-AED0-6B4893382D57}]
"Age"="30"
"Expression"="100"
"Age"=dword:0000001e
"Expression"=dword:00000064
"Gender"=""
"Head Size"="45"
"Lowering"="40"
"Head Size"=dword:0000002d
"Lowering"=dword:00000028
"Mode Name"="Kerry Watson"
"Pitch"="65"
"Prominences"="17"
"Pitch"=dword:00000041
"Prominences"=dword:00000011
"Speaker Name"="Kerry Watson"
"Speaking Rate"="32"
"Volume"="100"
"Speaking Rate"=dword:00000020
"Volume"=dword:00000064
EOF
echo "== registry"
+54 -32
View File
@@ -20,8 +20,7 @@ export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
export WINEARCH=win32
# VESKSLOP_TTS_VOICES entries: id|Name|gender|language[|flags]
# `no-pitch` marks voices whose engine ignores pitch (Flextalk).
VOICES="sam|Sam|male|en,mary|Mary|female|en,mike|Mike|male|en,keith-bell|Keith Bell|male|en|no-pitch,kerry-watson|Kerry Watson|female|en|no-pitch,mike-telephone|Mike (for Telephone)|male|en,mary-telephone|Mary (for Telephone)|female|en,mike-hall|Mike in Hall|male|en,mike-stadium|Mike in Stadium|male|en,mike-space|Mike in Space|male|en,mary-hall|Mary in Hall|female|en,mary-stadium|Mary in Stadium|female|en,mary-space|Mary in Space|female|en"
VOICES="sam|Sam|male|en,mary|Mary|female|en,mike|Mike|male|en,keith-bell|Keith Bell|male|en,kerry-watson|Kerry Watson|female|en,mike-telephone|Mike (for Telephone)|male|en,mary-telephone|Mary (for Telephone)|female|en,mike-hall|Mike in Hall|male|en,mike-stadium|Mike in Stadium|male|en,mike-space|Mike in Space|male|en,mary-hall|Mary in Hall|female|en,mary-stadium|Mary in Stadium|female|en,mary-space|Mary in Space|female|en"
if [ "${1:-}" = "--voices" ]; then
echo "$VOICES"
@@ -51,36 +50,38 @@ case "$VOICE" in
*) ENGVOICE="Sam" ;;
esac
# Engine units. Measured naturals: MSTTS ~150, Flextalk ~300 (words/min scale).
# Flextalk ignores ITTSAttributes pitch (verified byte-identical output).
# Engine units (measured). Speed is words/min, natural rate near 150 for every
# voice. Pitch units differ per voice: the engine's natural pitch corresponds to
# about 100 (MSTTS), 106 (Keith Bell) and 186 (Kerry Watson). Semitones scale
# that natural value by 2^(st/12) and clamp to the engine range.
case "$ENGVOICE" in
"Keith Bell"|"Kerry Watson") NAT_SPEED=300; SMAX=390 ;;
*) NAT_SPEED=150; SMAX=450 ;;
"Keith Bell") SMAX=390; PMAX=250; NAT_PITCH=106 ;;
"Kerry Watson") SMAX=390; PMAX=250; NAT_PITCH=186 ;;
*) SMAX=450; PMAX=200; NAT_PITCH=100 ;;
esac
SET_ARGS=""
if [ -n "$SPEED" ]; then
S=$(awk -v m="$SPEED" -v n="$NAT_SPEED" 'BEGIN{printf "%d", n*m+0.5}')
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
case "$ENGVOICE" in
"Keith Bell"|"Kerry Watson") : ;;
*)
P=$(awk -v st="$PITCH" 'BEGIN{printf "%d", 100 + st*6.25}')
[ "$P" -lt 50 ] && P=50
[ "$P" -gt 200 ] && P=200
SET_ARGS="$SET_ARGS -p $P" ;;
esac
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
# The daemon does not export DISPLAY; keep a private Xvfb for the engine.
if [ -z "${DISPLAY:-}" ]; then
start_xvfb() {
if ! [ -S /tmp/.X11-unix/X77 ]; then
Xvfb :77 -screen 0 1024x768x24 >/dev/null 2>&1 &
sleep 1
fi
}
if [ -z "${DISPLAY:-}" ]; then
start_xvfb
export DISPLAY=:77
fi
@@ -102,32 +103,53 @@ if [ ! -x "$HOST" ]; then HOST="$ROOT/build/sapi4host.exe"; fi
if [ ! -x "$HOST" ]; then echo "engine host not built" >&2; exit 1; fi
if ! command -v ffmpeg >/dev/null 2>&1; then echo "ffmpeg not found" >&2; exit 1; fi
# shellcheck disable=SC2086
if ! "$HOST" -v "$ENGVOICE" -t "$(cat "$TXT")" -o "$RAW" $SET_ARGS \
>/dev/null 2>"$TMP/err"; then
head -c 400 "$TMP/err" >&2
exit 1
run_engine() {
# shellcheck disable=SC2086
"$HOST" -v "$ENGVOICE" -t "$(cat "$TXT")" -o "$RAW" $SET_ARGS \
>/dev/null 2>"$TMP/err"
}
if ! run_engine; then
# A stale private Xvfb can break audio-destination creation; retry once.
if [ "${DISPLAY:-}" = ":77" ]; then
pkill -f "Xvfb :77" 2>/dev/null || true
rm -f /tmp/.X11-unix/X77
start_xvfb
if ! run_engine; then
head -c 400 "$TMP/err" >&2
exit 1
fi
else
head -c 400 "$TMP/err" >&2
exit 1
fi
fi
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }
SIL="silenceremove=start_periods=1:start_duration=0.05:start_silence=0.03:start_threshold=-50dB"
PRE="highpass=f=40,$SIL,afade=t=in:st=0:d=0.008,areverse,$SIL,afade=t=in:st=0:d=0.008,areverse"
LN="loudnorm=I=-16:TP=-3:LRA=11"
# Two-pass loudnorm: measure first, then apply with the measured values.
M=$(ffmpeg -hide_banner -nostats -i "$RAW" -af "$PRE,$LN:print_format=json" \
# Fixed-gain normalization: single-pass loudnorm misbehaves on short lines
# (LRA=0 causes heavy attenuation), so measure EBU R128 integrated loudness and
# true peak, then apply one gain clamped by the -3 dBFS peak target.
M=$(ffmpeg -hide_banner -nostats -i "$RAW" -af "$PRE,ebur128=peak=true" \
-f null - 2>&1 || true)
I=$(printf '%s' "$M" | sed -n 's/.*"input_i" *: *"\([^"]*\)".*/\1/p')
TP=$(printf '%s' "$M" | sed -n 's/.*"input_tp" *: *"\([^"]*\)".*/\1/p')
LRA=$(printf '%s' "$M" | sed -n 's/.*"input_lra" *: *"\([^"]*\)".*/\1/p')
TH=$(printf '%s' "$M" | sed -n 's/.*"input_thresh" *: *"\([^"]*\)".*/\1/p')
OFF=$(printf '%s' "$M" | sed -n 's/.*"target_offset" *: *"\([^"]*\)".*/\1/p')
if [ -n "$I" ] && [ -n "$TP" ] && [ -n "$LRA" ] && [ -n "$TH" ] && [ -n "$OFF" ]; then
LN="$LN:measured_I=$I:measured_TP=$TP:measured_LRA=$LRA:measured_thresh=$TH:offset=$OFF:linear=true"
I=$(printf '%s' "$M" | sed -n 's/ *I: *\(-\{0,1\}[0-9.]*\) *LUFS.*/\1/p' | tail -1)
TP=$(printf '%s' "$M" | sed -n 's/ *Peak: *\(-\{0,1\}[0-9.]*\) *dBFS.*/\1/p' | tail -1)
if [ -n "$I" ] && [ -n "$TP" ]; then
GAIN=$(awk -v i="$I" -v tp="$TP" 'BEGIN{
g = -16 - i;
lim = -3 - tp;
if (lim < g) g = lim;
if (g > 30) g = 30;
if (g < -20) g = -20;
printf "%.2f", g
}')
else
GAIN=0
fi
TMPOUT="$TMP/out.wav"
if ! ffmpeg -hide_banner -nostats -y -i "$RAW" -af "$PRE,$LN" \
if ! ffmpeg -hide_banner -nostats -y -i "$RAW" -af "$PRE,volume=${GAIN}dB" \
-ar 48000 -ac 1 -c:a pcm_s16le "$TMPOUT" >/dev/null 2>"$TMP/ff"; then
head -c 400 "$TMP/ff" >&2
exit 1