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