Newer
Older
* Asterisk -- An open source telephony toolkit.
* See http://www.asterisk.org for more information about
* the Asterisk project. Please do not directly contact
* any of the maintainers of this project for assistance;
* the project provides a web site, mailing lists and IRC
* channels for your use.
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/
* \brief Connect to festival
*
* \author Christos Ricudis <ricudis@itc.auth.gr>
*
* \extref The Festival Speech Synthesis System - http://www.cstr.ed.ac.uk/projects/festival/
/*! \li \ref app_festival.c uses the configuration file \ref festival.conf
* \addtogroup configuration_file Configuration Files
*/
* \page festival.conf festival.conf
* \verbinclude festival.conf.sample
*/
/*** MODULEINFO
<support_level>extended</support_level>
***/
Kevin P. Fleming
committed
#include "asterisk.h"
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <fcntl.h>
#include <ctype.h>
Kevin P. Fleming
committed
#include <errno.h>
#include "asterisk/file.h"
#include "asterisk/channel.h"
#include "asterisk/pbx.h"
#include "asterisk/module.h"
#include "asterisk/md5.h"
#include "asterisk/config.h"
#include "asterisk/utils.h"
#include "asterisk/lock.h"
Tilghman Lesher
committed
#include "asterisk/app.h"
#include "asterisk/format_cache.h"
Tilghman Lesher
committed
#define MAXLEN 180
#define MAXFESTLEN 2048
/*** DOCUMENTATION
<application name="Festival" language="en_US">
<synopsis>
Say text to the user.
</synopsis>
<syntax>
<parameter name="text" required="true" />
<parameter name="intkeys" />
</syntax>
<description>
<para>Connect to Festival, send the argument, get back the waveform, play it to the user,
allowing any given interrupt keys to immediately terminate and return the value, or
<literal>any</literal> to allow any number back (useful in dialplan).</para>
</description>
</application>
***/
Tilghman Lesher
committed
static char *socket_receive_file_to_buff(int fd, int *size)
Tilghman Lesher
committed
/* Receive file (probably a waveform file) from socket using
* Festival key stuff technique, but long winded I know, sorry
* but will receive any file without closing the stream or
* using OOB data
*/
static char *file_stuff_key = "ft_StUfF_key"; /* must == Festival's key */
char *buff, *tmp;
int bufflen;
int n,k,i;
char c;
Tilghman Lesher
committed
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
bufflen = 1024;
if (!(buff = ast_malloc(bufflen)))
return NULL;
*size = 0;
for (k = 0; file_stuff_key[k] != '\0';) {
n = read(fd, &c, 1);
if (n == 0)
break; /* hit stream eof before end of file */
if ((*size) + k + 1 >= bufflen) {
/* +1 so you can add a terminating NULL if you want */
bufflen += bufflen / 4;
if (!(tmp = ast_realloc(buff, bufflen))) {
ast_free(buff);
return NULL;
}
buff = tmp;
}
if (file_stuff_key[k] == c)
k++;
else if ((c == 'X') && (file_stuff_key[k+1] == '\0')) {
/* It looked like the key but wasn't */
for (i = 0; i < k; i++, (*size)++)
buff[*size] = file_stuff_key[i];
k = 0;
/* omit the stuffed 'X' */
} else {
for (i = 0; i < k; i++, (*size)++)
buff[*size] = file_stuff_key[i];
k = 0;
buff[*size] = c;
(*size)++;
}
}
return buff;
}
Tilghman Lesher
committed
static int send_waveform_to_fd(char *waveform, int length, int fd)
{
int res;
#if __BYTE_ORDER == __BIG_ENDIAN
Tilghman Lesher
committed
res = ast_safe_fork(0);
Tilghman Lesher
committed
if (res < 0)
ast_log(LOG_WARNING, "Fork failed\n");
if (res) {
return res;
}
Tilghman Lesher
committed
dup2(fd, 0);
ast_close_fds_above_n(0);
if (ast_opt_high_priority)
ast_set_priority(0);
#if __BYTE_ORDER == __BIG_ENDIAN
Tilghman Lesher
committed
for (x = 0; x < length; x += 2) {
c = *(waveform + x + 1);
*(waveform + x + 1) = *(waveform + x);
*(waveform + x) = c;
if (write(0, waveform, length) < 0) {
/* Cannot log -- all FDs are already closed */
Kevin P. Fleming
committed
}
Tilghman Lesher
committed
static int send_waveform_to_channel(struct ast_channel *chan, char *waveform, int length, char *intkeys)
{
int res = 0;
struct ast_format *owriteformat;
struct ast_frame *f;
struct myframe {
struct ast_frame f;
char offset[AST_FRIENDLY_OFFSET];
} myf = {
.f = { 0, },
};
Tilghman Lesher
committed
if (pipe(fds)) {
ast_log(LOG_WARNING, "Unable to create pipe\n");
return -1;
}
if (ast_channel_state(chan) != AST_STATE_UP)
owriteformat = ao2_bump(ast_channel_writeformat(chan));
res = ast_set_write_format(chan, ast_format_slin);
if (res < 0) {
ast_log(LOG_WARNING, "Unable to set write format to signed linear\n");
ao2_cleanup(owriteformat);
myf.f.frametype = AST_FRAME_VOICE;
myf.f.subclass.format = ast_format_slin;
myf.f.offset = AST_FRIENDLY_OFFSET;
myf.f.src = __PRETTY_FUNCTION__;
myf.f.data.ptr = myf.frdata;
Tilghman Lesher
committed
res = send_waveform_to_fd(waveform, length, fds[1]);
if (res >= 0) {
/* Order is important -- there's almost always going to be mp3... we want to prioritize the
user */
for (;;) {
Tilghman Lesher
committed
res = ast_waitfor(chan, 1000);
f = ast_read(chan);
if (!f) {
ast_log(LOG_WARNING, "Null frame == hangup() detected\n");
res = -1;
break;
}
if (f->frametype == AST_FRAME_DTMF) {
ast_debug(1, "User pressed a key\n");
if (intkeys && strchr(intkeys, f->subclass.integer)) {
res = f->subclass.integer;
}
if (f->frametype == AST_FRAME_VOICE) {
/* Treat as a generator */
if (needed > sizeof(myf.frdata)) {
ast_log(LOG_WARNING, "Only able to deliver %d of %d requested samples\n",
(int)sizeof(myf.frdata) / 2, needed/2);
res = read(fds[0], myf.frdata, needed);
if (res < needed) { /* last frame */
ast_debug(1, "Last frame\n");
Tilghman Lesher
committed
res = 0;
ast_debug(1, "No more waveform\n");
if (!res && owriteformat)
ast_set_write_format(chan, owriteformat);
ao2_cleanup(owriteformat);
static int festival_exec(struct ast_channel *chan, const char *vdata)
Tilghman Lesher
committed
int res = 0;
struct sockaddr_in serv_addr;
Tilghman Lesher
committed
const char *host;
const char *cachedir;
const char *temp;
const char *festivalcommand;
Tilghman Lesher
committed
int port = 1314;
int n;
char ack[4];
char *waveform;
int filesize;
char bigstring[MAXFESTLEN];
int i;
struct MD5Context md5ctx;
unsigned char MD5Res[16];
char MD5Hex[33] = "";
char koko[4] = "";
char cachefile[MAXFESTLEN]="";
Tilghman Lesher
committed
int readcache = 0;
int writecache = 0;
int seekpos = 0;
char *data;
Tilghman Lesher
committed
char *newfestivalcommand;
struct ast_flags config_flags = { 0 };
Tilghman Lesher
committed
AST_DECLARE_APP_ARGS(args,
AST_APP_ARG(text);
AST_APP_ARG(interrupt);
);
Russell Bryant
committed
if (ast_strlen_zero(vdata)) {
Russell Bryant
committed
ast_log(LOG_WARNING, "festival requires an argument (text)\n");
return -1;
}
cfg = ast_config_load(FESTIVAL_CONFIG, config_flags);
if (!cfg) {
ast_log(LOG_WARNING, "No such configuration file %s\n", FESTIVAL_CONFIG);
return -1;
Tilghman Lesher
committed
} else if (cfg == CONFIG_STATUS_FILEINVALID) {
ast_log(LOG_ERROR, "Config file " FESTIVAL_CONFIG " is in an invalid format. Aborting.\n");
return -1;
Tilghman Lesher
committed
if (!(host = ast_variable_retrieve(cfg, "general", "host"))) {
host = "localhost";
}
if (!(temp = ast_variable_retrieve(cfg, "general", "port"))) {
port = 1314;
} else {
port = atoi(temp);
}
if (!(temp = ast_variable_retrieve(cfg, "general", "usecache"))) {
Tilghman Lesher
committed
usecache = 0;
}
if (!(cachedir = ast_variable_retrieve(cfg, "general", "cachedir"))) {
cachedir = "/tmp/";
}
data = ast_strdupa(vdata);
AST_STANDARD_APP_ARGS(args, data);
if (!(festivalcommand = ast_variable_retrieve(cfg, "general", "festivalcommand"))) {
const char *startcmd = "(tts_textasterisk \"";
const char *endcmd = "\" 'file)(quit)\n";
strln = strlen(startcmd) + strlen(args.text) + strlen(endcmd) + 1;
newfestivalcommand = ast_alloca(strln);
snprintf(newfestivalcommand, strln, "%s%s%s", startcmd, args.text, endcmd);
festivalcommand = newfestivalcommand;
Tilghman Lesher
committed
} else { /* This else parses the festivalcommand that we're sent from the config file for \n's, etc */
int x, j;
newfestivalcommand = ast_alloca(strlen(festivalcommand) + strlen(args.text) + 1);
Tilghman Lesher
committed
for (x = 0, j = 0; x < strlen(festivalcommand); x++) {
if (festivalcommand[x] == '\\' && festivalcommand[x + 1] == 'n') {
Tilghman Lesher
committed
newfestivalcommand[j++] = '\n';
x++;
} else if (festivalcommand[x] == '\\') {
newfestivalcommand[j++] = festivalcommand[x + 1];
x++;
} else if (festivalcommand[x] == '%' && festivalcommand[x + 1] == 's') {
sprintf(&newfestivalcommand[j], "%s", args.text); /* we know it is big enough */
j += strlen(args.text);
Tilghman Lesher
committed
} else
newfestivalcommand[j++] = festivalcommand[x];
Tilghman Lesher
committed
}
newfestivalcommand[j] = '\0';
festivalcommand = newfestivalcommand;
Tilghman Lesher
committed
if (args.interrupt && !strcasecmp(args.interrupt, "any"))
args.interrupt = AST_DIGIT_ANY;
ast_debug(1, "Text passed to festival server : %s\n", args.text);
fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
Tilghman Lesher
committed
ast_log(LOG_WARNING, "festival_client: can't get socket\n");
return -1;
memset(&serv_addr, 0, sizeof(serv_addr));
if ((serv_addr.sin_addr.s_addr = inet_addr(host)) == -1) {
/* its a name rather than an ipnum */
serverhost = ast_gethostbyname(host, &ahp);
Tilghman Lesher
committed
if (serverhost == NULL) {
ast_log(LOG_WARNING, "festival_client: gethostbyname failed\n");
return -1;
}
Tilghman Lesher
committed
memmove(&serv_addr.sin_addr, serverhost->h_addr, serverhost->h_length);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
if (connect(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) != 0) {
Tilghman Lesher
committed
ast_log(LOG_WARNING, "festival_client: connect to server failed\n");
Tilghman Lesher
committed
return -1;
}
/* Compute MD5 sum of string */
MD5Init(&md5ctx);
MD5Update(&md5ctx, (unsigned char *)args.text, strlen(args.text));
MD5Final(MD5Res, &md5ctx);
MD5Hex[0] = '\0';
/* Convert to HEX and look if there is any matching file in the cache
Tilghman Lesher
committed
directory */
for (i = 0; i < 16; i++) {
snprintf(koko, sizeof(koko), "%X", (unsigned)MD5Res[i]);
Tilghman Lesher
committed
strncat(MD5Hex, koko, sizeof(MD5Hex) - strlen(MD5Hex) - 1);
}
readcache = 0;
writecache = 0;
if (strlen(cachedir) + strlen(MD5Hex) + 1 <= MAXFESTLEN && (usecache == -1)) {
snprintf(cachefile, sizeof(cachefile), "%s/%s", cachedir, MD5Hex);
fdesc = open(cachefile, O_RDWR);
if (fdesc == -1) {
fdesc = open(cachefile, O_CREAT | O_RDWR, AST_FILE_MODE);
if (fdesc != -1) {
writecache = 1;
strln = strlen(args.text);
ast_debug(1, "line length : %d\n", strln);
Kevin P. Fleming
committed
if (write(fdesc,&strln,sizeof(int)) < 0) {
ast_log(LOG_WARNING, "write() failed: %s\n", strerror(errno));
}
if (write(fdesc,data,strln) < 0) {
ast_log(LOG_WARNING, "write() failed: %s\n", strerror(errno));
}
Tilghman Lesher
committed
seekpos = lseek(fdesc, 0, SEEK_CUR);
ast_debug(1, "Seek position : %d\n", seekpos);
}
} else {
Kevin P. Fleming
committed
if (read(fdesc,&strln,sizeof(int)) != sizeof(int)) {
ast_log(LOG_WARNING, "read() failed: %s\n", strerror(errno));
}
Tilghman Lesher
committed
ast_debug(1, "Cache file exists, strln=%d, strlen=%d\n", strln, (int)strlen(args.text));
if (strlen(args.text) == strln) {
ast_debug(1, "Size OK\n");
Kevin P. Fleming
committed
if (read(fdesc,&bigstring,strln) != strln) {
ast_log(LOG_WARNING, "read() failed: %s\n", strerror(errno));
}
Tilghman Lesher
committed
bigstring[strln] = 0;
if (strcmp(bigstring, args.text) == 0) {
Tilghman Lesher
committed
readcache = 1;
} else {
ast_log(LOG_WARNING, "Strings do not match\n");
}
} else {
ast_log(LOG_WARNING, "Size mismatch\n");
}
}
Tilghman Lesher
committed
if (readcache == 1) {
Tilghman Lesher
committed
fd = fdesc;
ast_debug(1, "Reading from cache...\n");
Tilghman Lesher
committed
ast_debug(1, "Passing text to festival...\n");
fs = fdopen(dup(fd), "wb");
fprintf(fs, "%s", festivalcommand);
Tilghman Lesher
committed
if (writecache == 1) {
ast_debug(1, "Writing result to cache...\n");
while ((strln = read(fd, buffer, 16384)) != 0) {
Kevin P. Fleming
committed
if (write(fdesc,buffer,strln) < 0) {
ast_log(LOG_WARNING, "write() failed: %s\n", strerror(errno));
}
Tilghman Lesher
committed
fd = open(cachefile, O_RDWR);
lseek(fd, seekpos, SEEK_SET);
Tilghman Lesher
committed
ast_debug(1, "Passing data to channel...\n");
/* Read back info from server */
/* This assumes only one waveform will come back, also LP is unlikely */
do {
Tilghman Lesher
committed
int read_data;
for (n = 0; n < 3; ) {
read_data = read(fd, ack + n, 3 - n);
/* this avoids falling in infinite loop
* in case that festival server goes down
*/
if (read_data == -1) {
ast_log(LOG_WARNING, "Unable to read from cache/festival fd\n");
close(fd);
ast_config_destroy(cfg);
return -1;
}
n += read_data;
}
Tilghman Lesher
committed
if (strcmp(ack, "WV\n") == 0) { /* receive a waveform */
ast_debug(1, "Festival WV command\n");
if ((waveform = socket_receive_file_to_buff(fd, &filesize))) {
res = send_waveform_to_channel(chan, waveform, filesize, args.interrupt);
ast_free(waveform);
}
Tilghman Lesher
committed
} else if (strcmp(ack, "LP\n") == 0) { /* receive an s-expr */
ast_debug(1, "Festival LP command\n");
if ((waveform = socket_receive_file_to_buff(fd, &filesize))) {
waveform[filesize] = '\0';
ast_log(LOG_WARNING, "Festival returned LP : %s\n", waveform);
ast_free(waveform);
}
} else if (strcmp(ack, "ER\n") == 0) { /* server got an error */
ast_log(LOG_WARNING, "Festival returned ER\n");
res = -1;
Tilghman Lesher
committed
}
} while (strcmp(ack, "OK\n") != 0);
static int unload_module(void)
return ast_unregister_application(app);
/*!
* \brief Load the module
*
* Module loading including tests for configuration or dependencies.
* This function can return AST_MODULE_LOAD_FAILURE, AST_MODULE_LOAD_DECLINE,
* or AST_MODULE_LOAD_SUCCESS. If a dependency or environment variable fails
* tests return AST_MODULE_LOAD_FAILURE. If the module can not load the
* configuration file or other non-critical problem return
* AST_MODULE_LOAD_DECLINE. On success return AST_MODULE_LOAD_SUCCESS.
*/
static int load_module(void)
struct ast_flags config_flags = { 0 };
struct ast_config *cfg = ast_config_load(FESTIVAL_CONFIG, config_flags);
if (!cfg) {
ast_log(LOG_WARNING, "No such configuration file %s\n", FESTIVAL_CONFIG);
return AST_MODULE_LOAD_DECLINE;
Tilghman Lesher
committed
} else if (cfg == CONFIG_STATUS_FILEINVALID) {
ast_log(LOG_ERROR, "Config file " FESTIVAL_CONFIG " is in an invalid format. Aborting.\n");
return AST_MODULE_LOAD_DECLINE;
}
ast_config_destroy(cfg);
return ast_register_application_xml(app, festival_exec);
Mark Michelson
committed
AST_MODULE_INFO_STANDARD_EXTENDED(ASTERISK_GPL_KEY, "Simple Festival Interface");