wrapper: leading low hum to prime Discord (configurable MS/HZ/DB)

This commit is contained in:
Fisto
2026-09-16 13:21:41 +02:00
parent f9f1c454d5
commit 1f77dfba64
4 changed files with 47 additions and 1 deletions
+4
View File
@@ -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
+5
View File
@@ -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`)
+9
View File
@@ -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
View File
@@ -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