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, 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
|
||||
|
||||
- `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
|
||||
>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).
|
||||
|
||||
## 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,
|
||||
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.
|
||||
- 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
|
||||
|
||||
|
||||
+29
-1
@@ -20,6 +20,9 @@
|
||||
# VESKSLOP_TTS_WORD_GAP_MS silence between words in ms (0-500)
|
||||
# VESKSLOP_TTS_VOX 1 = VOX prosody, NetTTS-style (Flextalk)
|
||||
# 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.
|
||||
set -eu
|
||||
@@ -52,6 +55,9 @@ WORDS=${VESKSLOP_TTS_WORDS:-0}
|
||||
WORD_GAP=${VESKSLOP_TTS_WORD_GAP_MS:-120}
|
||||
VOX=${VESKSLOP_TTS_VOX:-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
|
||||
sam) ENGVOICE="Sam" ;;
|
||||
@@ -377,7 +383,29 @@ else
|
||||
fi
|
||||
|
||||
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
|
||||
head -c 400 "$TMP/ff" >&2
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user