From 1f77dfba64a88f4df42fb5a6e25710dc0d89e2e8 Mon Sep 17 00:00:00 2001 From: Fisto Date: Wed, 16 Sep 2026 13:21:41 +0200 Subject: [PATCH] wrapper: leading low hum to prime Discord (configurable MS/HZ/DB) --- README.md | 4 ++++ docs/NOTES.md | 5 +++++ docs/TTS-DESIGN.md | 9 +++++++++ setup/veskslop-tts.sh | 30 +++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f56a4d..ab4a527 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/NOTES.md b/docs/NOTES.md index 5c92ff7..8e16d2d 100644 --- a/docs/NOTES.md +++ b/docs/NOTES.md @@ -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`) diff --git a/docs/TTS-DESIGN.md b/docs/TTS-DESIGN.md index 558d358..80ed606 100644 --- a/docs/TTS-DESIGN.md +++ b/docs/TTS-DESIGN.md @@ -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 diff --git a/setup/veskslop-tts.sh b/setup/veskslop-tts.sh index 929f690..db0fec3 100755 --- a/setup/veskslop-tts.sh +++ b/setup/veskslop-tts.sh @@ -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