Tech Notes

Tech Note #20180002

There are number of situations where it may be useful to produce a spoken weather forecast that a user can listen to. On a phone switch (e.g. Asterisk), the switch can play the current weather to a user when they dial *61. At an unattended airport, a robot can transmit the weather information over the radio when an inbound pilot keys their microphone a number of times. A Web site may wish to play an audio version of the weather forecast for vision-impaired users. We are sure that there are numerous other places where audio weather forecasts would be of use.

Producing an audio forecast breaks down into three problems. The first is how to obtain weather information from a reliable source. Currently, there are several free or low-cost sources on the Internet where one can obtain weather information. Until recently, wunderground was one such source but, since its purchase by IBM, it is no longer free. For current weather, OpenWeatherMap (our favorite) and DarkSky provide free weather data in limited quantities (i.e. less than 1000 requests per day). If you update your weather data every 15 minutes, you should never exceed their quotas. For weather forecasts, the National Weather Service allows requests for them in limited quantities.

The second problem is how to format the weather information so that it will play well as an audio stream. This Tech Note includes the source code for the Perl program FetchWeather.pl that downloads weather data from the weather sources mentioned in the previous paragraph. It formats that data into a playable text stream and then runs it through a text to speech program to produce a wave file, which can be played by the phone switch, radio transmitter, etc.

The third problem is the text to speech conversion, which we handle with either Festival or flite, both of which are available for free from the Internet.

Source Code

You can download the source code for FetchWeather.pl here.

Installing FetchWeather.pl

You will need to have the Perl interpreter installed on the system where you will run this program. On most Linux systems, it is a foregone conclusion that it will already be installed. Under Windows, we prefer Strawberry Perl.

There are a couple of prerequisites that must be installed through CPAN, before this program will work. Install them something like this:


su
perl -MCPAN -e shell
install JSON
install Weather::Underground

Note that you can omit Weather::Underground if you won't be using wunderground.

Many of the weather sites are now insiting on using HTTPS for communications with the rest of us (the National Weather Office even has a mandate from the Office of Management and Budget [OMB Directive M-15-13] requiring it to do so). Now that weather forecasts are deemed to be a secret, we must make sure that our programs can communicate using SSL/TLS. We prefer to install IO::Socket::SSL for this job:


su
perl -MCPAN -e shell
install IO::Socket::SSL

Unzip the source module like this:


gunzip <FetchWeather-1.0.pl.gz >FetchWeather.pl

Assuming that you have a user named "monitor" that holds all of your system monitoring stuff, and that you want to make the weather forecast available to Asterisk, you might do something like this to install it:


su
mkdir /home/monitor/weather
cp FetchWeather.pl /home/monitor/weather/FetchWeather.pl
chown -R monitor:monitor /home/monitor/weather
chmod -R g+w /home/monitor/weather
chmod ugo+x /home/monitor/weather/FetchWeather.pl
ln -s /home/monitor/weather/Weather-MyTown.wav \
  /etc/asterisk/local/weather.wav

Running FetchWeather.pl

We add an entry to crontab to fetch the current weather conditions and forecast once every fifteen minutes. For example:


#
# Get the current weather conditions and forecast from the weather service
# every 15 minutes.
#
05,20,35,50 * * * * monitor /home/monitor/weather/FetchWeather.pl \
  --Forecast --Place="My Town, State" --Provider=OpenWeatherMap \
  --TTSProg=fest /home/monitor/weather/Weather-MyTown >/dev/null 2>&1

Using Festival

Most Linux systems include a packaged version of Festival which can be installed through their package manager. For example, on CentOS, you can do:


su
yum install festival

This should put a copy of text2wave in /usr/bin where FetchWeather.pl can find it. If your system installs text2wave in that directory, you're all set. Otherwise, you'll have to change the command string in the variable $WAVEGENFEST.

Using Flite

We build flite from source so that we can get the latest version of it. Also, we've modified the code for the flite program to allow us to change the sample rate to 8000 (required by some versions of Asterisk).

Flite is included in the build of Festival, which you can build using the general build shell script provided on the Festival Web site. This script (fest_build) will download all of the appropriate files and do the build. To run it and do the build (for Festival 2.5.0, which includes flite, for example):


mkdir festival-2.5.0
cd festival-2.5.0
cp .../fest_build  (to save a copy of the build used for this version)
chmod ugo+x fest_build
./fest_build

Note that in the above script, we change it to add "--prefix=/usr/local/festival --sysconfdir=/etc/festival" to the configure scripts for festival and festlite. This puts the binaries and config files where we want them.

Some versions of Asterisk require wave files with a sampling rate of 8000. By default the flite library and the flite program produce waveforms with a sampling rate of 16000. The following patches produce files with required rate.


--- main/flite_main.c.orig  2017-10-21 10:01:07.000000000 -0400
+++ main/flite_main.c.new   2018-08-12 12:10:20.000000000 -0400
@@ -83,6 +83,7 @@
            "  -f TEXTFILE Explicitly set input filename\n"
            "  -t TEXT     Explicitly set input textstring\n"
            "  -p PHONES   Explicitly set input textstring and synthesize as phones\n"
+           "  -r samprate Set sampling rate (default 16000), for output files only\n"
            "  --set F=V   Set feature (guesses type)\n"
            "  -s F=V      Set feature (guesses type)\n"
            "  --seti F=V  Set int feature\n"
@@ -203,6 +204,7 @@
     double time_start, time_end;
     int flite_verbose, flite_loop, flite_bench;
     int explicit_filename, explicit_text, explicit_phones, ssml_mode;
+    int samprate;  //ew
 #define ITER_MAX 3
     int bench_iter = 0;
     cst_features *extra_feats;
@@ -215,6 +217,7 @@
     flite_verbose = FALSE;
     flite_loop = FALSE;
     flite_bench = FALSE;
+    samprate = 16000;  //ew
     explicit_text = explicit_filename = explicit_phones = FALSE;
     ssml_mode = FALSE;
     extra_feats = new_features();
@@ -348,6 +351,11 @@
             explicit_phones = TRUE;
             i++;
         }
+        else if (cst_streq(argv[i],"-r") && (i+1 < argc))  //ew
+        {  //ew
+            samprate = atoi(argv[i+1]);  //ew
+            i++;  //ew
+        }  //ew
         else if (cst_streq(argv[i],"-t") && (i+1 < argc))
         {
             filename = argv[i+1];
@@ -410,8 +418,8 @@
     {
         if (ssml_mode)
             durs = flite_ssml_file_to_speech(filename,v,outtype);
-        else
-            durs = flite_file_to_speech(filename,v,outtype);
+        else
+            durs = flite_file_to_speech(filename,v,outtype,samprate);  //ew
     }

     gettimeofday(&tv,NULL);

--- src/synth/flite.c.orig  2017-10-21 10:01:07.000000000 -0400
+++ src/synth/flite.c.new   2018-08-12 12:10:56.000000000 -0400
@@ -248,7 +248,8 @@
 
 float flite_file_to_speech(const char *filename, 
                            cst_voice *voice,
-                           const char *outtype)
+                           const char *outtype,
+                           int samprate)  //ew
 {
     cst_tokenstream *ts;
 
@@ -263,13 +264,14 @@
                    filename);
         return 1;
     }
-    return flite_ts_to_speech(ts,voice,outtype);
+    return flite_ts_to_speech(ts,voice,outtype,samprate);  //ew
 }
 
 
 float flite_ts_to_speech(cst_tokenstream *ts,
                          cst_voice *voice,
-                         const char *outtype)
+                         const char *outtype,
+                         int samprate)  //ew
 {
     cst_utterance *utt;
     const char *token;
@@ -297,9 +299,10 @@
         !cst_streq(outtype,"none") &&
         !cst_streq(outtype,"stream"))
     {
+        if ((samprate < 4000) || (samprate > 48000)) samprate = 16000;  //ew
         w = new_wave();
         cst_wave_resize(w,0,1);
-        cst_wave_set_sample_rate(w,16000);
+        cst_wave_set_sample_rate(w,samprate);  //ew
         cst_wave_save_riff(w,outtype);  /* an empty wave */
         delete_wave(w);
     }
@@ -327,7 +330,7 @@
                     delete_utterance(utt); utt = NULL;
                     break;
                 }
-                durs += flite_process_output(utt,outtype,TRUE);
+                durs += flite_process_output(utt,outtype,TRUE,samprate);  //ew
                 delete_utterance(utt); utt = NULL;
             }
             else 
@@ -367,7 +370,7 @@
     float dur;
 
     u = flite_synth_text(text,voice);
-    dur = flite_process_output(u,outtype,FALSE);
+    dur = flite_process_output(u,outtype,FALSE,0);  //ew
     delete_utterance(u);
 
     return dur;
@@ -381,14 +384,14 @@
     float dur;
 
     u = flite_synth_phones(text,voice);
-    dur = flite_process_output(u,outtype,FALSE);
+    dur = flite_process_output(u,outtype,FALSE,0);  //ew
     delete_utterance(u);
 
     return dur;
 }
 
 float flite_process_output(cst_utterance *u, const char *outtype,
-                           int append)
+                           int append, int samprate)  //ew
 {
     /* Play or save (append) output to output file */
     cst_wave *w;
@@ -398,6 +401,10 @@
 
     w = utt_wave(u);
 
+    /* If the desired sample rate is different, resample utterance to it */
+    if ((samprate != 0) && (w->sample_rate != samprate))  //ew
+        cst_wave_resample(w, samprate);  //ew
+
     dur = (float)w->num_samples/(float)w->sample_rate;

     if (cst_streq(outtype,"play"))

--- src/synth/cst_ssml.c.orig  2017-10-21 10:01:07.000000000 -0400
+++ src/synth/cst_ssml.c.new   2018-08-12 12:26:57.000000000 -0400
@@ -396,7 +396,7 @@
                     delete_utterance(utt); utt = NULL;
                     break;
                 }
-                durs += flite_process_output(utt,outtype,TRUE);
+                durs += flite_process_output(utt,outtype,TRUE,0);  //ew
                 delete_utterance(utt); utt = NULL;
             }
             else 
@@ -418,7 +418,7 @@
             utt = utt_synth_wave(copy_wave(wave),current_voice);
             if (utt_user_callback)
                 utt = (utt_user_callback)(utt);
-            durs += flite_process_output(utt,outtype,TRUE);
+            durs += flite_process_output(utt,outtype,TRUE,0);  //ew
             delete_utterance(utt); utt = NULL;
 
             utt = new_utterance();

--- include/flite.h.orig    2017-10-21 10:01:07.000000000 -0400
+++ include/flite.h.new     2018-08-12 11:59:52.000000000 -0400
@@ -84,7 +84,8 @@
 int flite_voice_dump(cst_voice *voice, const char *voice_filename);
 float flite_file_to_speech(const char *filename, 
                            cst_voice *voice,
-                           const char *outtype);
+                           const char *outtype,
+                           int samprate);
 float flite_text_to_speech(const char *text, 
                            cst_voice *voice,
                            const char *outtype);
@@ -106,13 +107,15 @@
 
 float flite_ts_to_speech(cst_tokenstream *ts, 
                          cst_voice *voice,
-                         const char *outtype);
+                         const char *outtype,
+                         int samprate);  //ew
 cst_utterance *flite_do_synth(cst_utterance *u,
                               cst_voice *voice,
                               cst_uttfunc synth);
 float flite_process_output(cst_utterance *u,
                            const char *outtype,
-                           int append);
+                           int append,
+                           int samprate);  //ew
 
 /* for voices with external voxdata */
 int flite_mmap_clunit_voxdata(const char *voxdir, cst_voice *voice);

After applying these patches as you see fit (we just use our text editor), you can redo the flite build from the build/flite directory, perhaps like this:


cd .../festival-2.5.0/build/flite
make

If you are happy that the flite command will accept the rate parameter (e.g. "-r 8000"), you can install it from the same directory:


su
make install

Setting Up A Weather Service

To use the weather services such as OpenWeatherMap or DarkSky, you will need an API key. Obtain the appropriate API key at:

    https://darksky.net/dev/login
    https://home.openweathermap.org/users/sign_in

Insert the API key string into the appropriate $APIKEY variable in the code.

The easiest way to resolve latitude and longitude for a particular place is to look it's lat/lon up once on the Internet and then add an entry to the @Places array in FetchWeather.pl. The same applies for the LocationID key for OpenWeatherMap. If you choose to support weather data for any place, you'll probably need to get an API key from Google for geocoding place names. Good luck with that. Insert the API key into the $GEOKEY variable in the code.

Getting Asterisk To Play The Weather Forecast

If you followed our suggestion in the "Installing FetchWeather.pl" section, you would have added the symlink "/etc/asterisk/local/weather.wav" to point to the wave file that is generated by FetchWeather.pl every fifteen minutes.

To have Asterisk play this file upon request, one can add the following to the dial plan:


;;;;;;;;;;;;;;;;;;;;  Feature Code 61  ;;;;;;;;;;;;;;;;;;;;

; Give the current weather.
exten => ${SPECIALCODE}61,1,Answer
exten => ${SPECIALCODE}61,n,Playback(/etc/asterisk/local/weather)
exten => ${SPECIALCODE}61,n,Wait(1)
exten => ${SPECIALCODE}61,n,Hangup

The variable ${SPECIALCODE} is set to whatever your special code initiator is, typically '*'.