Create a new config file status, CONFIG_STATUS_FILEINVALID for differentiating
[asterisk/asterisk.git] / apps / app_festival.c
1 /*
2  * Asterisk -- An open source telephony toolkit.
3  *
4  * Copyright (C) 2002, Christos Ricudis
5  *
6  * Christos Ricudis <ricudis@itc.auth.gr>
7  *
8  * See http://www.asterisk.org for more information about
9  * the Asterisk project. Please do not directly contact
10  * any of the maintainers of this project for assistance;
11  * the project provides a web site, mailing lists and IRC
12  * channels for your use.
13  *
14  * This program is free software, distributed under the terms of
15  * the GNU General Public License Version 2. See the LICENSE file
16  * at the top of the source tree.
17  */
18
19 /*! \file
20  *
21  * \brief Connect to festival
22  *
23  * \author Christos Ricudis <ricudis@itc.auth.gr>
24  *
25  * \extref  The Festival Speech Synthesis System - http://www.cstr.ed.ac.uk/projects/festival/
26  * 
27  * \ingroup applications
28  */
29
30 #include "asterisk.h"
31
32 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
33
34 #include <sys/socket.h>
35 #include <netdb.h>
36 #include <netinet/in.h>
37 #include <arpa/inet.h>
38 #include <signal.h>
39 #include <fcntl.h>
40 #include <ctype.h>
41
42 #include "asterisk/file.h"
43 #include "asterisk/channel.h"
44 #include "asterisk/pbx.h"
45 #include "asterisk/module.h"
46 #include "asterisk/md5.h"
47 #include "asterisk/config.h"
48 #include "asterisk/utils.h"
49 #include "asterisk/lock.h"
50 #include "asterisk/app.h"
51
52 #define FESTIVAL_CONFIG "festival.conf"
53 #define MAXLEN 180
54 #define MAXFESTLEN 2048
55
56 static char *app = "Festival";
57
58 static char *synopsis = "Say text to the user";
59
60 static char *descrip = 
61 "  Festival(text[,intkeys]):  Connect to Festival, send the argument, get back the waveform,\n"
62 "play it to the user, allowing any given interrupt keys to immediately terminate and return\n"
63 "the value, or 'any' to allow any number back (useful in dialplan)\n";
64
65
66 static char *socket_receive_file_to_buff(int fd, int *size)
67 {
68         /* Receive file (probably a waveform file) from socket using
69          * Festival key stuff technique, but long winded I know, sorry
70          * but will receive any file without closing the stream or
71          * using OOB data
72          */
73         static char *file_stuff_key = "ft_StUfF_key"; /* must == Festival's key */
74         char *buff, *tmp;
75         int bufflen;
76         int n,k,i;
77         char c;
78
79         bufflen = 1024;
80         if (!(buff = ast_malloc(bufflen)))
81                 return NULL;
82         *size = 0;
83
84         for (k = 0; file_stuff_key[k] != '\0';) {
85                 n = read(fd, &c, 1);
86                 if (n == 0)
87                         break;  /* hit stream eof before end of file */
88                 if ((*size) + k + 1 >= bufflen) {
89                         /* +1 so you can add a terminating NULL if you want */
90                         bufflen += bufflen / 4;
91                         if (!(tmp = ast_realloc(buff, bufflen))) {
92                                 ast_free(buff);
93                                 return NULL;
94                         }
95                         buff = tmp;
96                 }
97                 if (file_stuff_key[k] == c)
98                         k++;
99                 else if ((c == 'X') && (file_stuff_key[k+1] == '\0')) {
100                         /* It looked like the key but wasn't */
101                         for (i = 0; i < k; i++, (*size)++)
102                                 buff[*size] = file_stuff_key[i];
103                         k = 0;
104                         /* omit the stuffed 'X' */
105                 } else {
106                         for (i = 0; i < k; i++, (*size)++)
107                                 buff[*size] = file_stuff_key[i];
108                         k = 0;
109                         buff[*size] = c;
110                         (*size)++;
111                 }
112         }
113
114         return buff;
115 }
116
117 static int send_waveform_to_fd(char *waveform, int length, int fd)
118 {
119         int res;
120 #ifdef __PPC__ 
121         char c;
122 #endif
123
124         res = ast_safe_fork(0);
125         if (res < 0)
126                 ast_log(LOG_WARNING, "Fork failed\n");
127         if (res) {
128                 return res;
129         }
130         dup2(fd, 0);
131         ast_close_fds_above_n(0);
132         if (ast_opt_high_priority)
133                 ast_set_priority(0);
134 #ifdef __PPC__  
135         for (x = 0; x < length; x += 2) {
136                 c = *(waveform + x + 1);
137                 *(waveform + x + 1) = *(waveform + x);
138                 *(waveform + x) = c;
139         }
140 #endif
141         write(fd, waveform, length);
142         close(fd);
143         exit(0);
144 }
145
146 static int send_waveform_to_channel(struct ast_channel *chan, char *waveform, int length, char *intkeys)
147 {
148         int res = 0;
149         int fds[2];
150         int pid = -1;
151         int needed = 0;
152         int owriteformat;
153         struct ast_frame *f;
154         struct myframe {
155                 struct ast_frame f;
156                 char offset[AST_FRIENDLY_OFFSET];
157                 char frdata[2048];
158         } myf = {
159                 .f = { 0, },
160         };
161
162         if (pipe(fds)) {
163                 ast_log(LOG_WARNING, "Unable to create pipe\n");
164                 return -1;
165         }
166
167         /* Answer if it's not already going */
168         if (chan->_state != AST_STATE_UP)
169                 ast_answer(chan);
170         ast_stopstream(chan);
171         ast_indicate(chan, -1);
172         
173         owriteformat = chan->writeformat;
174         res = ast_set_write_format(chan, AST_FORMAT_SLINEAR);
175         if (res < 0) {
176                 ast_log(LOG_WARNING, "Unable to set write format to signed linear\n");
177                 return -1;
178         }
179         
180         res = send_waveform_to_fd(waveform, length, fds[1]);
181         if (res >= 0) {
182                 pid = res;
183                 /* Order is important -- there's almost always going to be mp3...  we want to prioritize the
184                    user */
185                 for (;;) {
186                         res = ast_waitfor(chan, 1000);
187                         if (res < 1) {
188                                 res = -1;
189                                 break;
190                         }
191                         f = ast_read(chan);
192                         if (!f) {
193                                 ast_log(LOG_WARNING, "Null frame == hangup() detected\n");
194                                 res = -1;
195                                 break;
196                         }
197                         if (f->frametype == AST_FRAME_DTMF) {
198                                 ast_debug(1, "User pressed a key\n");
199                                 if (intkeys && strchr(intkeys, f->subclass)) {
200                                         res = f->subclass;
201                                         ast_frfree(f);
202                                         break;
203                                 }
204                         }
205                         if (f->frametype == AST_FRAME_VOICE) {
206                                 /* Treat as a generator */
207                                 needed = f->samples * 2;
208                                 if (needed > sizeof(myf.frdata)) {
209                                         ast_log(LOG_WARNING, "Only able to deliver %d of %d requested samples\n",
210                                                 (int)sizeof(myf.frdata) / 2, needed/2);
211                                         needed = sizeof(myf.frdata);
212                                 }
213                                 res = read(fds[0], myf.frdata, needed);
214                                 if (res > 0) {
215                                         myf.f.frametype = AST_FRAME_VOICE;
216                                         myf.f.subclass = AST_FORMAT_SLINEAR;
217                                         myf.f.datalen = res;
218                                         myf.f.samples = res / 2;
219                                         myf.f.offset = AST_FRIENDLY_OFFSET;
220                                         myf.f.src = __PRETTY_FUNCTION__;
221                                         myf.f.data.ptr = myf.frdata;
222                                         if (ast_write(chan, &myf.f) < 0) {
223                                                 res = -1;
224                                                 ast_frfree(f);
225                                                 break;
226                                         }
227                                         if (res < needed) { /* last frame */
228                                                 ast_debug(1, "Last frame\n");
229                                                 res = 0;
230                                                 ast_frfree(f);
231                                                 break;
232                                         }
233                                 } else {
234                                         ast_debug(1, "No more waveform\n");
235                                         res = 0;
236                                 }
237                         }
238                         ast_frfree(f);
239                 }
240         }
241         close(fds[0]);
242         close(fds[1]);
243
244 #if 0
245         if (pid > -1)
246                 kill(pid, SIGKILL);
247 #endif
248         if (!res && owriteformat)
249                 ast_set_write_format(chan, owriteformat);
250         return res;
251 }
252
253 static int festival_exec(struct ast_channel *chan, void *vdata)
254 {
255         int usecache;
256         int res = 0;
257         struct sockaddr_in serv_addr;
258         struct hostent *serverhost;
259         struct ast_hostent ahp;
260         int fd;
261         FILE *fs;
262         const char *host;
263         const char *cachedir;
264         const char *temp;
265         const char *festivalcommand;
266         int port = 1314;
267         int n;
268         char ack[4];
269         char *waveform;
270         int filesize;
271         int wave;
272         char bigstring[MAXFESTLEN];
273         int i;
274         struct MD5Context md5ctx;
275         unsigned char MD5Res[16];
276         char MD5Hex[33] = "";
277         char koko[4] = "";
278         char cachefile[MAXFESTLEN]="";
279         int readcache = 0;
280         int writecache = 0;
281         int strln;
282         int fdesc = -1;
283         char buffer[16384];
284         int seekpos = 0;        
285         char *data;     
286         struct ast_config *cfg;
287         char *newfestivalcommand;
288         struct ast_flags config_flags = { 0 };
289         AST_DECLARE_APP_ARGS(args,
290                 AST_APP_ARG(text);
291                 AST_APP_ARG(interrupt);
292         );
293
294         if (ast_strlen_zero(vdata)) {
295                 ast_log(LOG_WARNING, "festival requires an argument (text)\n");
296                 return -1;
297         }
298
299         cfg = ast_config_load(FESTIVAL_CONFIG, config_flags);
300         if (!cfg) {
301                 ast_log(LOG_WARNING, "No such configuration file %s\n", FESTIVAL_CONFIG);
302                 return -1;
303         } else if (cfg == CONFIG_STATUS_FILEINVALID) {
304                 ast_log(LOG_ERROR, "Config file " FESTIVAL_CONFIG " is in an invalid format.  Aborting.\n");
305                 return -1;
306         }
307
308         if (!(host = ast_variable_retrieve(cfg, "general", "host"))) {
309                 host = "localhost";
310         }
311         if (!(temp = ast_variable_retrieve(cfg, "general", "port"))) {
312                 port = 1314;
313         } else {
314                 port = atoi(temp);
315         }
316         if (!(temp = ast_variable_retrieve(cfg, "general", "usecache"))) {
317                 usecache = 0;
318         } else {
319                 usecache = ast_true(temp);
320         }
321         if (!(cachedir = ast_variable_retrieve(cfg, "general", "cachedir"))) {
322                 cachedir = "/tmp/";
323         }
324
325         data = ast_strdupa(vdata);
326         AST_STANDARD_APP_ARGS(args, data);
327
328         if (!(festivalcommand = ast_variable_retrieve(cfg, "general", "festivalcommand"))) {
329                 const char *startcmd = "(tts_textasterisk \"";
330                 const char *endcmd = "\" 'file)(quit)\n";
331
332                 strln = strlen(startcmd) + strlen(args.text) + strlen(endcmd) + 1;
333                 newfestivalcommand = alloca(strln);
334                 snprintf(newfestivalcommand, strln, "%s%s%s", startcmd, args.text, endcmd);
335                 festivalcommand = newfestivalcommand;
336         } else { /* This else parses the festivalcommand that we're sent from the config file for \n's, etc */
337                 int x, j;
338                 newfestivalcommand = alloca(strlen(festivalcommand) + strlen(args.text) + 1);
339
340                 for (x = 0, j = 0; x < strlen(festivalcommand); x++) {
341                         if (festivalcommand[x] == '\\' && festivalcommand[x + 1] == 'n') {
342                                 newfestivalcommand[j++] = '\n';
343                                 x++;
344                         } else if (festivalcommand[x] == '\\') {
345                                 newfestivalcommand[j++] = festivalcommand[x + 1];
346                                 x++;
347                         } else if (festivalcommand[x] == '%' && festivalcommand[x + 1] == 's') {
348                                 sprintf(&newfestivalcommand[j], "%s", args.text); /* we know it is big enough */
349                                 j += strlen(args.text);
350                                 x++;
351                         } else
352                                 newfestivalcommand[j++] = festivalcommand[x];
353                 }
354                 newfestivalcommand[j] = '\0';
355                 festivalcommand = newfestivalcommand;
356         }
357         
358         if (args.interrupt && !strcasecmp(args.interrupt, "any"))
359                 args.interrupt = AST_DIGIT_ANY;
360
361         ast_debug(1, "Text passed to festival server : %s\n", args.text);
362         /* Connect to local festival server */
363         
364         fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
365
366         if (fd < 0) {
367                 ast_log(LOG_WARNING, "festival_client: can't get socket\n");
368                 ast_config_destroy(cfg);
369                 return -1;
370         }
371
372         memset(&serv_addr, 0, sizeof(serv_addr));
373
374         if ((serv_addr.sin_addr.s_addr = inet_addr(host)) == -1) {
375                 /* its a name rather than an ipnum */
376                 serverhost = ast_gethostbyname(host, &ahp);
377
378                 if (serverhost == NULL) {
379                         ast_log(LOG_WARNING, "festival_client: gethostbyname failed\n");
380                         ast_config_destroy(cfg);
381                         return -1;
382                 }
383                 memmove(&serv_addr.sin_addr, serverhost->h_addr, serverhost->h_length);
384         }
385
386         serv_addr.sin_family = AF_INET;
387         serv_addr.sin_port = htons(port);
388
389         if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) {
390                 ast_log(LOG_WARNING, "festival_client: connect to server failed\n");
391                 ast_config_destroy(cfg);
392                 return -1;
393         }
394
395         /* Compute MD5 sum of string */
396         MD5Init(&md5ctx);
397         MD5Update(&md5ctx, (unsigned char *)args.text, strlen(args.text));
398         MD5Final(MD5Res, &md5ctx);
399         MD5Hex[0] = '\0';
400
401         /* Convert to HEX and look if there is any matching file in the cache 
402                 directory */
403         for (i = 0; i < 16; i++) {
404                 snprintf(koko, sizeof(koko), "%X", MD5Res[i]);
405                 strncat(MD5Hex, koko, sizeof(MD5Hex) - strlen(MD5Hex) - 1);
406         }
407         readcache = 0;
408         writecache = 0;
409         if (strlen(cachedir) + strlen(MD5Hex) + 1 <= MAXFESTLEN && (usecache == -1)) {
410                 snprintf(cachefile, sizeof(cachefile), "%s/%s", cachedir, MD5Hex);
411                 fdesc = open(cachefile, O_RDWR);
412                 if (fdesc == -1) {
413                         fdesc = open(cachefile, O_CREAT | O_RDWR, AST_FILE_MODE);
414                         if (fdesc != -1) {
415                                 writecache = 1;
416                                 strln = strlen(args.text);
417                                 ast_debug(1, "line length : %d\n", strln);
418                                 write(fdesc, &strln, sizeof(strln));
419                                 write(fdesc, args.text, strln);
420                                 seekpos = lseek(fdesc, 0, SEEK_CUR);
421                                 ast_debug(1, "Seek position : %d\n", seekpos);
422                         }
423                 } else {
424                         read(fdesc, &strln, sizeof(strln));
425                         ast_debug(1, "Cache file exists, strln=%d, strlen=%d\n", strln, (int)strlen(args.text));
426                         if (strlen(args.text) == strln) {
427                                 ast_debug(1, "Size OK\n");
428                                 read(fdesc, &bigstring, strln);
429                                 bigstring[strln] = 0;
430                                 if (strcmp(bigstring, args.text) == 0) { 
431                                         readcache = 1;
432                                 } else {
433                                         ast_log(LOG_WARNING, "Strings do not match\n");
434                                 }
435                         } else {
436                                 ast_log(LOG_WARNING, "Size mismatch\n");
437                         }
438                 }
439         }
440
441         if (readcache == 1) {
442                 close(fd);
443                 fd = fdesc;
444                 ast_debug(1, "Reading from cache...\n");
445         } else {
446                 ast_debug(1, "Passing text to festival...\n");
447                 fs = fdopen(dup(fd), "wb");
448
449                 fprintf(fs, "%s", festivalcommand);
450                 fflush(fs);
451                 fclose(fs);
452         }
453         
454         /* Write to cache and then pass it down */
455         if (writecache == 1) {
456                 ast_debug(1, "Writing result to cache...\n");
457                 while ((strln = read(fd, buffer, 16384)) != 0) {
458                         write(fdesc, buffer, strln);
459                 }
460                 close(fd);
461                 close(fdesc);
462                 fd = open(cachefile, O_RDWR);
463                 lseek(fd, seekpos, SEEK_SET);
464         }
465         
466         ast_debug(1, "Passing data to channel...\n");
467
468         /* Read back info from server */
469         /* This assumes only one waveform will come back, also LP is unlikely */
470         wave = 0;
471         do {
472                 int read_data;
473                 for (n = 0; n < 3; ) {
474                         read_data = read(fd, ack + n, 3 - n);
475                         /* this avoids falling in infinite loop
476                          * in case that festival server goes down
477                          */
478                         if (read_data == -1) {
479                                 ast_log(LOG_WARNING, "Unable to read from cache/festival fd\n");
480                                 close(fd);
481                                 ast_config_destroy(cfg);
482                                 return -1;
483                         }
484                         n += read_data;
485                 }
486                 ack[3] = '\0';
487                 if (strcmp(ack, "WV\n") == 0) {         /* receive a waveform */
488                         ast_debug(1, "Festival WV command\n");
489                         if ((waveform = socket_receive_file_to_buff(fd, &filesize))) {
490                                 res = send_waveform_to_channel(chan, waveform, filesize, args.interrupt);
491                                 ast_free(waveform);
492                         }
493                         break;
494                 } else if (strcmp(ack, "LP\n") == 0) {   /* receive an s-expr */
495                         ast_debug(1, "Festival LP command\n");
496                         if ((waveform = socket_receive_file_to_buff(fd, &filesize))) {
497                                 waveform[filesize] = '\0';
498                                 ast_log(LOG_WARNING, "Festival returned LP : %s\n", waveform);
499                                 ast_free(waveform);
500                         }
501                 } else if (strcmp(ack, "ER\n") == 0) {    /* server got an error */
502                         ast_log(LOG_WARNING, "Festival returned ER\n");
503                         res = -1;
504                         break;
505                 }
506         } while (strcmp(ack, "OK\n") != 0);
507         close(fd);
508         ast_config_destroy(cfg);
509         return res;
510 }
511
512 static int unload_module(void)
513 {
514         return ast_unregister_application(app);
515 }
516
517 static int load_module(void)
518 {
519         struct ast_flags config_flags = { 0 };
520         struct ast_config *cfg = ast_config_load(FESTIVAL_CONFIG, config_flags);
521         if (!cfg) {
522                 ast_log(LOG_WARNING, "No such configuration file %s\n", FESTIVAL_CONFIG);
523                 return AST_MODULE_LOAD_DECLINE;
524         } else if (cfg == CONFIG_STATUS_FILEINVALID) {
525                 ast_log(LOG_ERROR, "Config file " FESTIVAL_CONFIG " is in an invalid format.  Aborting.\n");
526                 return AST_MODULE_LOAD_DECLINE;
527         }
528         ast_config_destroy(cfg);
529         return ast_register_application(app, festival_exec, synopsis, descrip);
530 }
531
532 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Simple Festival Interface");