wrapper: leading low hum to prime Discord (configurable MS/HZ/DB)
This commit is contained in:
@@ -89,6 +89,10 @@ command environment (`VESKSLOP_TTS_VOX_CLEAN=1` omits the `\!wH1/\!wH0` stutter
|
|||||||
wrapper). Line-leading `/rate N` and `/pitch N` controls are also handled by the
|
wrapper). Line-leading `/rate N` and `/pitch N` controls are also handled by the
|
||||||
wrapper, so the WebUI does not need to implement them. See `docs/TTS-DESIGN.md`.
|
wrapper, so the WebUI does not need to implement them. See `docs/TTS-DESIGN.md`.
|
||||||
|
|
||||||
|
A short low-level leading hum (80 ms, 75 Hz, -48 dBFS) primes Discord's audio pipeline
|
||||||
|
so the first word is not gated; tune with `VESKSLOP_TTS_HUM_MS` (0 = off),
|
||||||
|
`VESKSLOP_TTS_HUM_HZ`, and `VESKSLOP_TTS_HUM_DB`.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `host/sapi4host.c` — host: voice select, pitch/speed, text -> WAV
|
- `host/sapi4host.c` — host: voice select, pitch/speed, text -> WAV
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ without them.
|
|||||||
silence is retained, so it shapes silence, not speech. Measured head: first sample
|
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
|
>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.
|
~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).
|
||||||
|
|
||||||
## SAPI4 runtime engine discovery (`speech.dll`)
|
## SAPI4 runtime engine discovery (`speech.dll`)
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,15 @@ record what the engine needs to interpret, add, or hide._
|
|||||||
Measured on a Keith Bell line: first sample >30 at 22.6 ms, speech (>500) at 29.6 ms,
|
Measured on a Keith Bell line: first sample >30 at 22.6 ms, speech (>500) at 29.6 ms,
|
||||||
attack rising naturally over about 15 ms, tail decaying over about 18 ms. No click and
|
attack rising naturally over about 15 ms, tail decaying over about 18 ms. No click and
|
||||||
no attenuation of the first word; Veskslop's 150 ms playback lead-in sits on top.
|
no attenuation of the first word; Veskslop's 150 ms playback lead-in sits on top.
|
||||||
|
- Leading hum (on by default): 80 ms of a 75 Hz sine at -48 dBFS peak is prepended
|
||||||
|
before the engine padding, to keep Discord's audio pipeline open so the first word is
|
||||||
|
not gated. Configurable: `VESKSLOP_TTS_HUM_MS` (0 disables), `_HUM_HZ` (30-200),
|
||||||
|
`_HUM_DB` (-80..-6). It sits about 45 dB under the speech peak; raise `_HUM_DB` toward
|
||||||
|
-36 if a client still gates the start, lower it for stealth. Generated in the same
|
||||||
|
ffmpeg pass (no extra process) and not part of the EBU measurement, so the documented
|
||||||
|
loudness/peak targets still hold. Measured: hum 75.0 Hz / -48.0 dBFS, speech onset at
|
||||||
|
about 110 ms, final -18.6 LUFS / -3.0 dBFS; `HUM_MS=0` reproduces the previous output
|
||||||
|
byte-for-byte.
|
||||||
|
|
||||||
### Voice IDs
|
### Voice IDs
|
||||||
|
|
||||||
|
|||||||
+29
-1
@@ -20,6 +20,9 @@
|
|||||||
# VESKSLOP_TTS_WORD_GAP_MS silence between words in ms (0-500)
|
# VESKSLOP_TTS_WORD_GAP_MS silence between words in ms (0-500)
|
||||||
# VESKSLOP_TTS_VOX 1 = VOX prosody, NetTTS-style (Flextalk)
|
# VESKSLOP_TTS_VOX 1 = VOX prosody, NetTTS-style (Flextalk)
|
||||||
# VESKSLOP_TTS_VOX_CLEAN 1 = VOX without the \!wH1/\!wH0 stutter
|
# VESKSLOP_TTS_VOX_CLEAN 1 = VOX without the \!wH1/\!wH0 stutter
|
||||||
|
# VESKSLOP_TTS_HUM_MS leading low hum in ms, 0 = off (default 80)
|
||||||
|
# VESKSLOP_TTS_HUM_HZ hum frequency, 30-200 (default 75)
|
||||||
|
# VESKSLOP_TTS_HUM_DB hum peak level, -80..-6 (default -48)
|
||||||
#
|
#
|
||||||
# Output: 48 kHz mono s16le WAV, trimmed, faded, loudness-normalized.
|
# Output: 48 kHz mono s16le WAV, trimmed, faded, loudness-normalized.
|
||||||
set -eu
|
set -eu
|
||||||
@@ -52,6 +55,9 @@ WORDS=${VESKSLOP_TTS_WORDS:-0}
|
|||||||
WORD_GAP=${VESKSLOP_TTS_WORD_GAP_MS:-120}
|
WORD_GAP=${VESKSLOP_TTS_WORD_GAP_MS:-120}
|
||||||
VOX=${VESKSLOP_TTS_VOX:-0}
|
VOX=${VESKSLOP_TTS_VOX:-0}
|
||||||
VOX_CLEAN=${VESKSLOP_TTS_VOX_CLEAN:-0}
|
VOX_CLEAN=${VESKSLOP_TTS_VOX_CLEAN:-0}
|
||||||
|
HUM_MS=${VESKSLOP_TTS_HUM_MS:-80}
|
||||||
|
HUM_HZ=${VESKSLOP_TTS_HUM_HZ:-75}
|
||||||
|
HUM_DB=${VESKSLOP_TTS_HUM_DB:--48}
|
||||||
|
|
||||||
case "$VOICE" in
|
case "$VOICE" in
|
||||||
sam) ENGVOICE="Sam" ;;
|
sam) ENGVOICE="Sam" ;;
|
||||||
@@ -377,7 +383,29 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
TMPOUT="$TMP/out.wav"
|
TMPOUT="$TMP/out.wav"
|
||||||
if ! "$MICROSLOP_FFMPEG" -hide_banner -nostats -y -i "$RAW" -af "$PRE,volume=${GAIN}dB" \
|
# Leading hum: a quiet low tone in the pre-speech padding, to prime Discord's
|
||||||
|
# audio pipeline so the first syllable is not gated. Level/frequency/duration
|
||||||
|
# are configurable; 0 ms disables it.
|
||||||
|
case "$HUM_MS" in *[!0-9]*|"") HUM_MS=80 ;; esac
|
||||||
|
[ "$HUM_MS" -gt 2000 ] && HUM_MS=2000
|
||||||
|
case "$HUM_HZ" in *[!0-9.]*|"") HUM_HZ=75 ;; esac
|
||||||
|
case "$HUM_DB" in *[!0-9.-]*|""|"."|"-") HUM_DB=-48 ;; esac
|
||||||
|
if [ "$HUM_MS" -gt 0 ]; then
|
||||||
|
HUM_S=$(awk -v ms="$HUM_MS" 'BEGIN{printf "%.3f", ms/1000}')
|
||||||
|
HUM_HZ=$(awk -v v="$HUM_HZ" 'BEGIN{ if (v<30) v=30; if (v>200) v=200; printf "%.1f", v }')
|
||||||
|
HUM_DB=$(awk -v v="$HUM_DB" 'BEGIN{ if (v>-6) v=-6; if (v<-80) v=-80; printf "%.1f", v }')
|
||||||
|
HUM_AMP=$(awk -v db="$HUM_DB" 'BEGIN{printf "%.6f", 10^(db/20)}')
|
||||||
|
HUM_FADE=$(awk -v s="$HUM_S" 'BEGIN{ f=s/4; if (f>0.02) f=0.02; if (f<0.005) f=0.005; printf "%.3f", f }')
|
||||||
|
HUM_FADE_OUT=$(awk -v s="$HUM_S" -v f="$HUM_FADE" 'BEGIN{printf "%.3f", s-f}')
|
||||||
|
if ! "$MICROSLOP_FFMPEG" -hide_banner -nostats -y \
|
||||||
|
-f lavfi -i "aevalsrc=exprs='$HUM_AMP*sin(2*PI*$HUM_HZ*t)':s=48000:d=$HUM_S" \
|
||||||
|
-i "$RAW" \
|
||||||
|
-filter_complex "[0:a]afade=t=in:st=0:d=$HUM_FADE,afade=t=out:st=$HUM_FADE_OUT:d=$HUM_FADE,aformat=sample_fmts=s16:channel_layouts=mono:sample_rates=48000[h];[1:a]$PRE,volume=${GAIN}dB,aresample=48000,aformat=sample_fmts=s16:channel_layouts=mono[a];[h][a]concat=n=2:v=0:a=1[out]" \
|
||||||
|
-map "[out]" -c:a pcm_s16le "$TMPOUT" >/dev/null 2>"$TMP/ff"; then
|
||||||
|
head -c 400 "$TMP/ff" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif ! "$MICROSLOP_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
|
-ar 48000 -ac 1 -c:a pcm_s16le "$TMPOUT" >/dev/null 2>"$TMP/ff"; then
|
||||||
head -c 400 "$TMP/ff" >&2
|
head -c 400 "$TMP/ff" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user