#!/bin/bash
set -e

# configure voice and extra-args for the tts engine;
# expensive: used for the static message,
#     cheap: used for the timestamp
#
# voice is either a name from -lv, or path to .flitevox file

expensive=(
  ~/Downloads/cmu_us_slt.flitevox
  --setf duration_stretch=0.9 --setf int_f0_target_stddev=30)

cheap=(
  slt --setf duration_stretch=0.9 --setf int_f0_target_stddev=30)

#--setf f0_shift=1.02
#--setf int_f0_target_mean=180 \

# end config
#


ptag="$1"
msg="$2"
[ -z "$2" ] && {
    echo "need arg 1: pipe name"
    echo "need arg 2: message to loop"
    echo
    echo "example:"
    echo "  mkfifo /dev/shm/tts.tower.mp3"
    echo "  killall -USR1 icecast"
    echo "  $0 tower 'relay 3 lost connection to tower'"
    exit 1
}

base=/dev/shm/tts.$ptag

[ -e $base.mp3 ] || {
    echo "first mkfifo and start reading from $base.mp3"
    exit 1
}

rm -f $base.wav{0,1}
touch $base.alive

log() {
    printf '\033[1;37m%s> \033[0;3%sm%s\033[0m\n' \
        "$(date +'%Y-%m-%d, %H:%M:%S')" $1 "$2" >&2
}


log "2;1" "prebuilding message"
printf '%s\n' "$msg" |
flite -voice "${expensive[@]}" -pw -f /dev/stdin -o $base.msg
log "2;1" "nice"


# tts generator thread
timeshift=2
while true
do
    # FFmpeg renames wav1 while it consumes
    while [ -e $base.wav1 ] && [ -e $base.alive ]
    do
        sleep 0.1
    done

    [ -e $base.alive ] ||
        break

    ts="$(date +'%H:%M:%S' --date "+$timeshift seconds")"
    ts="$(echo $ts | sed -r 's/(:|^)0/ oh /g')"

    printf "at %s\n" "$ts" |
    flite -voice "${cheap[@]}" -pw -f /dev/stdin -o $base.wav0

    log 3 "flite out wav"
    tail -qc +45 $base.msg >> $base.wav0
    tail -qc +45 $base.wav0 > $base.wav1

    # 34284 byte = 1.07s
    filesz=$(stat -c%s $base.wav1)
    timeshift=$((filesz/32000))
    timeshift=$((timeshift+4))
done &


# ffmpeg thread
# flite output is pcm_s16le, 16000 Hz, mono, s16, 256 kb/s
# but icecast needs 44khz stereo to prevent listener drops
ffmpeg \
    -y -hide_banner -v error \
    -re -probesize 32 -analyzeduration 0 \
    -avioflags +direct -fflags +flush_packets \
    -f s16le -ar 16k -ac 1 -i - \
    -acodec libmp3lame -b:a 192k -ar 44100 -ac 2 $base.mp3 < <(

# cannot use pipe from while to ffmpeg
# as the while would be kept alive when ffmpeg exits
# however with process substitution we die correctly
while true
do
    # will only spin here during a ddos
    while [ ! -e $base.wav1 ] && [ -e $base.alive ]
    do
        sleep 0.1
    done

    [ -e $base.alive ] ||
        break

    log 5 "ffmpeg take wav"
    mv $base.wav{1,2}
    
    # byte 36 = magic "data"
    # byte 44 = first sample
    tail -c +45 $base.wav2
done
) || true


# cleanup
rm -f $base.mp3 $base.wav{0,1,2} $base.{alive,msg}
wait
rm -f $base.mp3 $base.wav{0,1,2}

exit 0





# test client: low latency
f=/dev/shm/tts.tower.mp3; rm -f $f && mkfifo $f && cat $f | ffplay -hide_banner -probesize 32 -

# test client: no audible glitches (latency ~1sec)
f=/dev/shm/tts.tower.mp3; rm -f $f && mkfifo $f && cat $f | mpv -

# stresstest
for n in range {1..10}; do ./ttsloop.sh $n 'lost tower uplink on relay '$n & done 
for n in {1..10}; do f=/dev/shm/tts.$n.mp3; rm -f $f && mkfifo $f && mpv $f -volume 30 & done 
ps aux | awk '/mpv [/]dev[/]shm/ {print $2}' | while IFS= read -r pid; do kill $pid; done

# performance checks
voices="kal kal16 awb rms slt ~/Downloads/cmu_us_slt.flitevox"
perfiles="1000 500 100 25 10 5 2"
runtest() {
rm /dev/shm/times.*
for perfile in $perfiles; do
rm /dev/shm/words.*; awk '!/[^a-zA-Z '\'']/ && NR%20==0' /usr/share/*/cracklib-small | head -n 1000 | split -a 4 -l $perfile - /dev/shm/words.
for f in /dev/shm/words.a*; do (time flite "$@" -f /dev/stdin -o /dev/shm/tmp.wav < $f) 2>&1 | tee -a /dev/shm/times.$perfile ; done
done
for f in /dev/shm/times.{1000,500,100,25,10,5,2}; do printf '%s = ' $f; awk -Fm '/^real/ {sub(/s$/,"");tot+=$2} END {print tot}' < $f; done | tee sumtime.${2##*/}
}
for voice in $voices; do runtest -voice $voice; done

# show stats
printf '%20s ' ''; for perfile in $perfiles; do printf '%6s ' $perfile; done; echo; for voice in $voices; do printf '%24s ' ${voice##*/}; for perfile in $perfiles; do awk '/times\.'$perfile' = / {printf "%6.2f ", $3}' sumtime.${voice##*/}; done; echo; done

                       1000    500    100     25     10      5      2 
                 kal   2.09   2.07   1.84   2.06   2.59   3.56   6.36  3.2x
               kal16   1.29   1.27   1.08   1.26   1.71   2.54   5.10  4.2x
                 awb  18.07  17.93  17.43  17.70  18.84  20.88  26.92  1.5x
                 rms  21.92  22.02  20.12  20.24  21.28  23.40  29.24  1.4x
                 slt  17.82  18.00  17.11  17.56  18.58  20.90  27.73  1.6x
 cmu_us_slt.flitevox  27.93  28.26  26.88  30.13  39.55  54.18  99.30  3.7x
