Standardized routines for forking processes (keeps all the specialized code in one...
[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 = 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         }
304         if (!(host = ast_variable_retrieve(cfg, "general", "host"))) {
305                 host = "localhost";
306         }
307         if (!(temp = ast_variable_retrieve(cfg, "general", "port"))) {
308                 port = 1314;
309         } else {
310                 port = atoi(temp);
311         }
312         if (!(temp = ast_variable_retrieve(cfg, "general", "usecache"))) {
313                 usecache = 0;
314         } else {
315                 usecache = ast_true(temp);
316         }
317         if (!(cachedir = ast_variable_retrieve(cfg, "general", "cachedir"))) {
318                 cachedir = "/tmp/";
319         }
320
321         data = ast_strdupa(vdata);
322         AST_STANDARD_APP_ARGS(args, data);
323
324         if (!(festivalcommand = ast_variable_retrieve(cfg, "general", "festivalcommand"))) {
325                 const char *startcmd = "(tts_textasterisk \"";
326                 const char *endcmd = "\" 'file)(quit)\n";
327
328                 strln = strlen(startcmd) + strlen(args.text) + strlen(endcmd) + 1;
329                 newfestivalcommand = alloca(strln);
330                 snprintf(newfestivalcommand, strln, "%s%s%s", startcmd, args.text, endcmd);
331                 festivalcommand = newfestivalcommand;
332         } else { /* This else parses the festivalcommand that we're sent from the config file for \n's, etc */
333                 int i, j;
334                 newfestivalcommand = alloca(strlen(festivalcommand) + strlen(args.text) + 1);
335
336                 for (i = 0, j = 0; i < strlen(festivalcommand); i++) {
337                         if (festivalcommand[i] == '\\' && festivalcommand[i + 1] == 'n') {
338                                 newfestivalcommand[j++] = '\n';
339                                 i++;
340                         } else if (festivalcommand[i] == '\\') {
341                                 newfestivalcommand[j++] = festivalcommand[i + 1];
342                                 i++;
343                         } else if (festivalcommand[i] == '%' && festivalcommand[i + 1] == 's') {
344                                 sprintf(&newfestivalcommand[j], "%s", args.text); /* we know it is big enough */
345                                 j += strlen(args.text);
346                                 i++;
347                         } else
348                                 newfestivalcommand[j++] = festivalcommand[i];
349                 }
350                 newfestivalcommand[j] = '\0';
351                 festivalcommand = newfestivalcommand;
352         }
353         
354         if (args.interrupt && !strcasecmp(args.interrupt, "any"))
355                 args.interrupt = AST_DIGIT_ANY;
356
357         ast_debug(1, "Text passed to festival server : %s\n", args.text);
358         /* Connect to local festival server */
359         
360         fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
361
362         if (fd < 0) {
363                 ast_log(LOG_WARNING, "festival_client: can't get socket\n");
364                 ast_config_destroy(cfg);
365                 return -1;
366         }
367
368         memset(&serv_addr, 0, sizeof(serv_addr));
369
370         if ((serv_addr.sin_addr.s_addr = inet_addr(host)) == -1) {
371                 /* its a name rather than an ipnum */
372                 serverhost = ast_gethostbyname(host, &ahp);
373
374                 if (serverhost == NULL) {
375                         ast_log(LOG_WARNING, "festival_client: gethostbyname failed\n");
376                         ast_config_destroy(cfg);
377                         return -1;
378                 }
379                 memmove(&serv_addr.sin_addr, serverhost->h_addr, serverhost->h_length);
380         }
381
382         serv_addr.sin_family = AF_INET;
383         serv_addr.sin_port = htons(port);
384
385         if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) {
386                 ast_log(LOG_WARNING, "festival_client: connect to server failed\n");
387                 ast_config_destroy(cfg);
388                 return -1;
389         }
390
391         /* Compute MD5 sum of string */
392         MD5Init(&md5ctx);
393         MD5Update(&md5ctx, (unsigned char *)args.text, strlen(args.text));
394         MD5Final(MD5Res, &md5ctx);
395         MD5Hex[0] = '\0';
396
397         /* Convert to HEX and look if there is any matching file in the cache 
398                 directory */
399         for (i = 0; i < 16; i++) {
400                 snprintf(koko, sizeof(koko), "%X", MD5Res[i]);
401                 strncat(MD5Hex, koko, sizeof(MD5Hex) - strlen(MD5Hex) - 1);
402         }
403         readcache = 0;
404         writecache = 0;
405         if (strlen(cachedir) + strlen(MD5Hex) + 1 <= MAXFESTLEN && (usecache == -1)) {
406                 snprintf(cachefile, sizeof(cachefile), "%s/%s", cachedir, MD5Hex);
407                 fdesc = open(cachefile, O_RDWR);
408                 if (fdesc == -1) {
409                         fdesc = open(cachefile, O_CREAT | O_RDWR, AST_FILE_MODE);
410                         if (fdesc != -1) {
411                                 writecache = 1;
412                                 strln = strlen(args.text);
413                                 ast_debug(1, "line length : %d\n", strln);
414                                 write(fdesc, &strln, sizeof(strln));
415                                 write(fdesc, args.text, strln);
416                                 seekpos = lseek(fdesc, 0, SEEK_CUR);
417                                 ast_debug(1, "Seek position : %d\n", seekpos);
418                         }
419                 } else {
420                         read(fdesc, &strln, sizeof(strln));
421                         ast_debug(1, "Cache file exists, strln=%d, strlen=%d\n", strln, (int)strlen(args.text));
422                         if (strlen(args.text) == strln) {
423                                 ast_debug(1, "Size OK\n");
424                                 read(fdesc, &bigstring, strln);
425                                 bigstring[strln] = 0;
426                                 if (strcmp(bigstring, args.text) == 0) { 
427                                         readcache = 1;
428                                 } else {
429                                         ast_log(LOG_WARNING, "Strings do not match\n");
430                                 }
431                         } else {
432                                 ast_log(LOG_WARNING, "Size mismatch\n");
433                         }
434                 }
435         }
436
437         if (readcache == 1) {
438                 close(fd);
439                 fd = fdesc;
440                 ast_debug(1, "Reading from cache...\n");
441         } else {
442                 ast_debug(1, "Passing text to festival...\n");
443                 fs = fdopen(dup(fd), "wb");
444
445                 fprintf(fs, "%s", festivalcommand);
446                 fflush(fs);
447                 fclose(fs);
448         }
449         
450         /* Write to cache and then pass it down */
451         if (writecache == 1) {
452                 ast_debug(1, "Writing result to cache...\n");
453                 while ((strln = read(fd, buffer, 16384)) != 0) {
454                         write(fdesc, buffer, strln);
455                 }
456                 close(fd);
457                 close(fdesc);
458                 fd = open(cachefile, O_RDWR);
459                 lseek(fd, seekpos, SEEK_SET);
460         }
461         
462         ast_debug(1, "Passing data to channel...\n");
463
464         /* Read back info from server */
465         /* This assumes only one waveform will come back, also LP is unlikely */
466         wave = 0;
467         do {
468                 int read_data;
469                 for (n = 0; n < 3; ) {
470                         read_data = read(fd, ack + n, 3 - n);
471                         /* this avoids falling in infinite loop
472                          * in case that festival server goes down
473                          */
474                         if (read_data == -1) {
475                                 ast_log(LOG_WARNING, "Unable to read from cache/festival fd\n");
476                                 close(fd);
477                                 ast_config_destroy(cfg);
478                                 return -1;
479                         }
480                         n += read_data;
481                 }
482                 ack[3] = '\0';
483                 if (strcmp(ack, "WV\n") == 0) {         /* receive a waveform */
484                         ast_debug(1, "Festival WV command\n");
485                         if ((waveform = socket_receive_file_to_buff(fd, &filesize))) {
486                                 res = send_waveform_to_channel(chan, waveform, filesize, args.interrupt);
487                                 ast_free(waveform);
488                         }
489                         break;
490                 } else if (strcmp(ack, "LP\n") == 0) {   /* receive an s-expr */
491                         ast_debug(1, "Festival LP command\n");
492                         if ((waveform = socket_receive_file_to_buff(fd, &filesize))) {
493                                 waveform[filesize] = '\0';
494                                 ast_log(LOG_WARNING, "Festival returned LP : %s\n", waveform);
495                                 ast_free(waveform);
496                         }
497                 } else if (strcmp(ack, "ER\n") == 0) {    /* server got an error */
498                         ast_log(LOG_WARNING, "Festival returned ER\n");
499                         res = -1;
500                         break;
501                 }
502         } while (strcmp(ack, "OK\n") != 0);
503         close(fd);
504         ast_config_destroy(cfg);
505         return res;
506 }
507
508 static int unload_module(void)
509 {
510         return ast_unregister_application(app);
511 }
512
513 static int load_module(void)
514 {
515         struct ast_flags config_flags = { 0 };
516         struct ast_config *cfg = ast_config_load(FESTIVAL_CONFIG, config_flags);
517         if (!cfg) {
518                 ast_log(LOG_WARNING, "No such configuration file %s\n", FESTIVAL_CONFIG);
519                 return AST_MODULE_LOAD_DECLINE;
520         }
521         ast_config_destroy(cfg);
522         return ast_register_application(app, festival_exec, synopsis, descrip);
523 }
524
525 AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Simple Festival Interface");