Time & Station ID Announcement (TOTH) in AzuraCast using Text-To-Speech

#azuracast #radio #linux #picotts

Many of us want (or are required to have) a periodic time and station ID announcement for our internet radio stations. We could make sound files for every hour and minute and write crazy long scripts to play those at the top of the hour (TOTH), but there is a much easier way:

How about having a simple MP3 file that you can use in your playlists any time and that always has the correct time and station ID?

Sample Time & Station ID Announcement in British English
Sample Time & Station ID Announcement in German
Sample in real station use (German)

I will show you how

We will use PicoTTS, a very lightweight and local Text-to-Speech engine, and sox to generate the MP3 file. Using a crontab job, we will regenerate the file every minute, so the time announcement is always current, whenever you play it.

PicoTTS supports British English, American English, German, French, Italian and Spanish. It runs offline and thus can be used on servers with no or restricted Internet access. With a little optimization in sox, the synthesized voices don’t sound too bad. They aren’t as good as online Google TTS using gTTS, but much better than eSpeak, Festival and pyttsx3, which I also tested.

All this will run in the host (so not within AzuraCast’s docker containers), and mount a folder into the station’s media folder that in turn contains the announcement MP3 files, ready to use.

You’ll need

Prepare the host system

Install PicoTTS and sox

SSH into your server and become root (sudo su). Then install the following:

apt install libttspico-utils sox libsox-fmt-mp3

Install any needed server language packs

Note: For any other language than English, the correct system locale must be installed on your server and specified in the script! Example installation for the German language pack:

apt install language-pack-de

Install and modify the scripts

We will use small bash scripts for each language desired (PicoTTS supports British English, American English, German, French, Italian and Spanish).

Here are sample scripts saytime-en and saytime-de for British English and German, respectively. Modify these as desired, adapt the station ID message, and don’t forget to set the correct time zone for your station. If desired, you can also modify the silence padding (default: 3s of silence at beginning and end).

You can use nano as an editor on the server. Save the scripts into the /usr/local/bin folder.

saytime-en

#!/bin/bash
# saytime-en
# 2015-07-16 Moonbase
# 2023-03-09 Moonbase (update to PicoTTS; pico2wave & sox must be installed)
# Create /tmp/time.en-GB.wav for Liquidsoap to announce current time.
# Put this script into your ~/bin folder (or ~/.local/bin, or /usr/local/bin).
# 2023-05-28 Moonbase Adapted to AzuraCast
# On a Ubuntu Server, install required modules as follows:
# sudo apt install libttspico-utils sox libsox-fmt-mp3
# Install this file into /usr/local/bin and use root's crontab to run every minute.
# 2023-05-29 Moonbase Simplified silence generation
#
# crontab entry, to keep the file current, like so:
#   */1 *  *   *   *     /usr/local/bin/saytime-en > /dev/null

# Possible output languages (might require different sox post-processing):
# de-DE - German
# en-GB - British Englisch
# en-US - US Englisch
# fr-FR - French
# it-IT - Italian
# es-ES - Spanish
lang="en-GB" # language for PicoTTS
locale="en_GB.utf8" # (installed!) OS locale
tz='Europe/Berlin' # desired timezone
# In case you wish to copy the file from /tmp into your media folder, specify it here.
# Use a trailing slash for folder names ("/folder/").
# You can also specify a full MP3 file name ("/folder/time-signal.mp3")
media_folder="/var/azuracast/time/"
# silence to add before/after the spoken text, in seconds; empty string for none
pad="pad 3 3"

# English
AMPM="$(LC_ALL=$locale TZ=$tz date +'%p')"
AMPM="${AMPM^^}" # uppercase it
TIME="$(LC_ALL=$locale TZ=$tz date +'%l:%M ')${AMPM}"
TOTH="$(LC_ALL=$locale TZ=$tz date +"%l o'clock ")${AMPM}"
MIN="$(LC_ALL=$locale TZ=$tz date +'%-M')"
DAY="$(LC_ALL=$locale TZ=$tz date +'%A')"
ZERO="oh" # to pronounce the zero when minutes < 10 ("three oh two PM")

# German
#AMPM="$(LC_ALL=$locale TZ=$tz date +'%p')"
#AMPM="${AMPM^^}" # uppercase it
#TIME="$(LC_ALL=$locale TZ=$tz date +'%-H Uhr %-M')"
#TOTH="$(LC_ALL=$locale TZ=$tz date +'genau %-H Uhr')"
#MIN="$(LC_ALL=$locale TZ=$tz date +'%-M')"
#DAY="$(LC_ALL=$locale TZ=$tz date +'%A')"
#ZERO="Uhr" # to pronounce the zero when minutes < 10 ("three oh two PM")

if [ "$MIN" -lt 10 ]; then
  TIME="$(LC_ALL=$locale TZ=$tz date +'%l') $ZERO $MIN $AMPM"
fi

if [ "$MIN" -eq 0 ]; then
  TIME="$TOTH"
fi

# diag only
#echo $DAY, $TIME

# English
# build text (adapt volume level 0..100)
text='<volume level="100">'"It's $DAY, $TIME, and you're listening to Night Radio.</volume>"

# German
# build text (adapt volume level 0..100), 2s silence at beginning and end
#text='<volume level="100">'"Es ist $DAY, $TIME, und du hörst Nait Räidio.</volume>"


# create WAV audio file using PicoTTS
pico2wave -l=$lang -w=/tmp/time.$lang.wav "$text"

# use sox to make output better understandable (voices are rather muffled)
# we also add some silence at the beginning and end here, if so requested
# adding some treble in the range of +3 to +6 dB helps
# some voices might need a little bass reduction, use s/th like "bass -6 400"
# to avoid clipping, give headroom (gain -h) and reclaim afterwards (gain -r)
# skip the "compand" part if you don’t want compression/limiting.
sox /tmp/time.$lang.wav -c2 -r 44100 -C 128 /tmp/time.$lang.mp3 gain -h bass +2 treble +3 gain -r compand 0.008,0.024 -60,-60,-40,-35,0,-20 10 -60 $pad
# copy it into our media folder
cp /tmp/time.$lang.mp3 "$media_folder"
# diag only: play it loud
#play /tmp/time.$lang.mp3

saytime-de

#!/bin/bash
# saytime-de
# 2015-07-16 Moonbase
# 2023-03-09 Moonbase (update to PicoTTS; pico2wave & sox must be installed)
# Create /tmp/time.en-GB.wav for Liquidsoap to announce current time.
# Put this script into your ~/bin folder (or ~/.local/bin, or /usr/local/bin).
# 2023-05-28 Moonbase Adapted to AzuraCast
# On a Ubuntu Server, install required modules as follows:
# sudo apt install libttspico-utils sox libsox-fmt-mp3
# Install this file into /usr/local/bin and use root's crontab to run every minute.
# 2023-05-29 Moonbase Simplified silence generation
#
# crontab entry, to keep the file current, like so:
#   */1 *  *   *   *     /usr/local/bin/saytime-de > /dev/null

# Possible output languages (might require different sox post-processing):
# de-DE - German
# en-GB - British Englisch
# en-US - US Englisch
# fr-FR - French
# it-IT - Italian
# es-ES - Spanish
lang="de-DE" # language for PicoTTS
locale="de_DE.utf8" # (installed!) OS locale
tz='Europe/Berlin' # desired timezone
# In case you wish to copy the file from /tmp into your media folder, specify it here.
# Use a trailing slash for folder names ("/folder/").
# You can also specify a full MP3 file name ("/folder/time-signal.mp3")
media_folder="/var/azuracast/time/"
# silence to add before/after the spoken text, in seconds; empty string for none
pad="pad 3 3"

# English
#AMPM="$(LC_ALL=$locale TZ=$tz date +'%p')"
#AMPM="${AMPM^^}" # uppercase it
#TIME="$(LC_ALL=$locale TZ=$tz date +'%l:%M ')${AMPM}"
#TOTH="$(LC_ALL=$locale TZ=$tz date +"%l o'clock ")${AMPM}"
#MIN="$(LC_ALL=$locale TZ=$tz date +'%-M')"
#DAY="$(LC_ALL=$locale TZ=$tz date +'%A')"
#ZERO="oh" # to pronounce the zero when minutes < 10 ("three oh two PM")

# German
AMPM="$(LC_ALL=$locale TZ=$tz date +'%p')"
AMPM="${AMPM^^}" # uppercase it
TIME="$(LC_ALL=$locale TZ=$tz date +'%-H Uhr %-M')"
TOTH="$(LC_ALL=$locale TZ=$tz date +'genau %-H Uhr')"
MIN="$(LC_ALL=$locale TZ=$tz date +'%-M')"
DAY="$(LC_ALL=$locale TZ=$tz date +'%A')"
ZERO="Uhr" # to pronounce the zero when minutes < 10 ("three oh two PM")

if [ "$MIN" -lt 10 ]; then
  TIME="$(LC_ALL=$locale TZ=$tz date +'%-H') $ZERO $MIN $AMPM"
fi

if [ "$MIN" -eq 0 ]; then
  TIME="$TOTH"
fi

# diag only
#echo $DAY, $TIME

# English
# build text (adapt volume level 0..100)
#text='<volume level="100">'"It's $DAY, $TIME, and you're listening to Night Radio.</volume>"

# German
# build text (adapt volume level 0..100), 2s silence at beginning and end
text='<volume level="100">'"Es ist $DAY, $TIME, und du hörst Nait Räidio.</volume>"


# create WAV audio file using PicoTTS
pico2wave -l=$lang -w=/tmp/time.$lang.wav "$text"

# use sox to make output better understandable (voices are rather muffled)
# we also add some silence at the beginning and end here, if so requested
# adding some treble in the range of +3 to +6 dB helps
# some voices might need a little bass reduction, use s/th like "bass -6 400"
# to avoid clipping, give headroom (gain -h) and reclaim afterwards (gain -r)
# skip the "compand" part if you don’t want compression/limiting.
sox /tmp/time.$lang.wav -c2 -r 44100 -C 128 /tmp/time.$lang.mp3 gain -h bass +2 treble +3 gain -r compand 0.008,0.024 -60,-60,-40,-35,0,-20 10 -60 $pad
# copy it into our media folder
cp /tmp/time.$lang.mp3 "$media_folder"
# diag only: play it loud
#play /tmp/time.$lang.mp3

Prepare a media folder time

This folder will hold the auto-generated time announcements time.de-DE.mp3 and time.en-GB.mp3. It will later be mounted into your Docker container, as the station’s media/time folder.

mkdir /var/azuracast/time

Test if the scripts work

saytime-de
saytime-en

You should now have two new announcement files in your /var/azuracast/time folder:

ls -l /var/azuracast/time
total 468
-rw-r--r-- 1 root root 193515 Mai 28 16:24 time.de-DE.mp3
-rw-r--r-- 1 root root 180976 Mai 28 16:24 time.en-GB.mp3

Mount the time folder into your station’s media folder

Let’s assume your station is called “niteradio”. Edit the file /var/azuracast/docker-compose.override.yml to include the new volume (but don’t modify other entries that might already be there):

nano /var/azuracast/docker-compose.override.yml

and add the new time folder to the station’s media folder:

services:
  web:
    volumes:
      - /var/azuracast/time:/var/azuracast/stations/niteradio/media/time

Now, restart your station:

cd /var/azuracast
docker compose down
docker compose up -d

In your station, under Media → Music Files, you should now see a new folder time

azuracast-time-folder

Open it, and check the files by playing them

azuracast-time-folder-files

You should hear your time and station ID announcements (but not current yet).

Set up crontab to regenerate the announcement every minute

Time for automation! Set up crontab jobs to regenerate the announcement files every minute, so you can forget them and just play them whenever needed.

As root, edit your crontab file and set everything up:

crontab -e

At the end, add the following lines (modify to your needs), and save:

*/1 * * * * /usr/local/bin/saytime-de > /dev/null
*/1 * * * * /usr/local/bin/saytime-en > /dev/null

Don’t worry, PicoTTS is rather leightweight, and will not eat up much CPU, even when regenerating your announcements every minute. And it’s local, so no extra Internet accesses are needed.

Prepare your station to use the announcements

Check if the files play the current time

Check if the cron job works, by playing each “time” announcement a few times. It should play the current time and your station ID, in all languages you have set up.

If something is wrong, check your /usr/local/bin/saytime-* scripts. Use the correct time zone for your station, be sure the system has the needed language pack(s) installed, and you used the correct specifier, i.e. de_DE.utf8.

Set up correct fading

For the time & station ID announcement files, set up correct fading because they will later interrupt the AutoDJ stream. Under Media → Media Files → time, click on Edit for each “time” file, go to Advanced and enter the following values:

azuracast-time-announcement-fading

This assumes you haven’t changed the default 3s silence padding at the start and end of the files.

Hint: Don’t use the Visual Cue Editor to set up fading for these files, since their lengths change every minute!

Create a Top-of-the-Hour playlist

If you wish to play a time & station ID at the top of the hour (TOTH), you can create a playlist like the following:

azuracast-toth-playlist

azuracast-toth-playlist-basic

azuracast-toth-playlist-advanced

Refine and adapt to your needs

You can always adapt and enhance the scripts, remove or add to the station IDs, and create wild playlists using the newly created announcements.

Since we installed PicoTTS, you could in theory now even use Liquidsoap’s pico2wave protocol and let it speak other things in your Liquidsoap script.

Further reading: SVOX Pico Manual.pdf

Enjoy!

Use, modify, steal, enhance, adapt to your heart’s content!

Don’t forget to share this, and possibly even add your great enhancements to the AzuraCast discussion thread.