/* * Asterisk -- An open source telephony toolkit. * * Copyright (c) 2007 Digium, Inc. * * Tilghman Lesher * * 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. * */ /*! \file * * \brief Speak application * * \author Tilghman Lesher * * \ingroup applications */ /*** MODULEINFO swift ***/ #include "asterisk.h" ASTERISK_FILE_VERSION(__FILE__, "$Revision: 77808 $") #include #include #include #include #include #include "asterisk/file.h" #include "asterisk/logger.h" #include "asterisk/options.h" #include "asterisk/channel.h" #include "asterisk/pbx.h" #include "asterisk/module.h" #include "asterisk/app.h" #define AST_MODULE "app_speak" static char *app_speak = "Speak"; static char *app_speak2 = "SpeakUntilExten"; static char *speak_synopsis = "Synthesizes spoken audio from a text string"; static char *speak2_synopsis = "Synthesizes spoken audio, while waiting for a new extension"; static char *speak_descrip = "Speak()\n" " Speak synthesizes a voice audio stream from the given text and plays that\n" "audio back to the channel.\n"; static char *speak2_descrip = "SpeakUntilExten()\n" " SpeakUntilExten synthesizes a voice audio stream from the given text and\n" "plays that audio back to the channel. When DTMF is detected on the channel,\n" "SpeakUntilExten pauses and waits for a complete new extension to be entered.\n"; typedef struct cst_item_contents_struct cst_item_contents; typedef struct cst_relation_struct cst_relation; typedef struct cst_item_struct cst_item; typedef struct cst_features_struct cst_features; typedef struct cst_utterance_struct cst_utterance; struct cst_item_struct { cst_item_contents *contents; /* the shared part of an item */ cst_relation *relation; cst_item *n; cst_item *p; cst_item *u; cst_item *d; }; struct cst_relation_struct { char *name; cst_features *features; cst_utterance *utterance; cst_item *head; cst_item *tail; }; cst_item *relation_head(cst_relation *r) { return ( r == NULL ? NULL : r->head); } typedef struct cst_regex_struct { char regstart; /* Internal use only. */ char reganch; /* Internal use only. */ char *regmust; /* Internal use only. */ int regmlen; /* Internal use only. */ int regsize; char *program; } cst_regex; static const unsigned char cst_rx_int_rxprog[] = { 156, 6, 0, 40, 1, 0, 3, 6, 0, 8, 8, 0, 8, 45, 0, 6, 0, 3, 9, 0, 3, 11, 0, 17, 4, 0, 0, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 0, 2, 0, 3, 0, 0, 0 }; static const cst_regex cst_rx_int_rx = { 0, 1, NULL, 0, 44, (char *) cst_rx_int_rxprog }; const cst_regex *cst_rx_int = &cst_rx_int_rx; struct callback_data { /* Raw buffer */ int buflen; char buf[8192]; /* Channel to write back audio frames */ struct ast_channel *chan; short voice_rate; /* Cepstral pointers */ swift_port *port; void *tid; /* Flag for which DTMF digit was pressed */ int dtmf; /* Flag for cancelling on DTMF */ unsigned int wantdtmf:1; }; static void queue_some_frames(struct callback_data *cbd, int final) { short *starting_sample, *sample; void *unconsumed = cbd->buf; int multiplier = cbd->voice_rate / 8000; struct ast_frame *f; int ms; /* Frame */ struct ast_frame fr = { .frametype = AST_FRAME_VOICE, .subclass = AST_FORMAT_SLINEAR, .datalen = 200, .samples = 100, .offset = AST_FRIENDLY_OFFSET, .src = __PRETTY_FUNCTION__ }; char __attribute__((unused)) unused[AST_FRIENDLY_OFFSET] = ""; short frdata[100]; fr.data = frdata; for (starting_sample = (void *)cbd->buf; (void *)starting_sample < (void *)cbd->buf + cbd->buflen - sizeof(frdata) * multiplier; starting_sample += sizeof(frdata) * multiplier) { if (multiplier == 1) { memcpy(frdata, starting_sample, sizeof(frdata)); unconsumed = (void *)starting_sample + sizeof(frdata); } else { int offset = 0; for (sample = starting_sample; (void *)sample < (void *)starting_sample + sizeof(frdata) * multiplier; sample += multiplier) frdata[offset++] = *sample; unconsumed = sample; } /* Write the frame */ if (ast_write(cbd->chan, &fr) < 0) break; /* Wait for this sample to complete */ ms = ast_waitfor(cbd->chan, 100); if (ms < 0) { if (option_debug) ast_log(LOG_DEBUG, "Hangup detected\n"); cbd->dtmf = -1; break; } else if (ms) { f = ast_read(cbd->chan); if (!f) { if (option_debug) ast_log(LOG_DEBUG, "Caught a NULL?!!\n"); cbd->dtmf = -1; break; } if (f->frametype == AST_FRAME_DTMF) { if (cbd->wantdtmf) { cbd->dtmf = f->subclass; ast_frfree(f); break; } } ast_frfree(f); } } if (final) { /* Queue any remaining data in the buffer */ fr.datalen = ((void *)cbd->buf + cbd->buflen - unconsumed) / multiplier; fr.samples = fr.datalen / 2; if (multiplier == 1) memcpy(frdata, unconsumed, fr.datalen); else { int offset = 0; for (sample = unconsumed; (void *)sample < unconsumed + fr.datalen * multiplier; sample += multiplier) frdata[offset++] = *sample; } unconsumed += fr.datalen * multiplier; /* Write the last frame */ ast_write(cbd->chan, &fr); } /* Update buffer pointers */ if (cbd->buf != unconsumed) { if (option_debug) ast_log(LOG_DEBUG, "Updating buffer pointers: %p, %p, %d\n", cbd->buf, unconsumed, (void *)cbd->buf - unconsumed + cbd->buflen); memcpy(cbd->buf, unconsumed, (void *)cbd->buf + cbd->buflen - unconsumed); cbd->buflen -= unconsumed - (void *)cbd->buf; } if (cbd->dtmf) { /* User pressed DTMF */ swift_result_t rv = swift_port_stop(cbd->port, cbd->tid, SWIFT_EVENT_NOW); if (rv != SWIFT_SUCCESS) ast_log(LOG_ERROR, "swift_port_stop() failed: %s\n", swift_strerror(rv)); } } static swift_result_t audio_callback(swift_event *event, swift_event_t type, void *udata) { struct callback_data *cbd = udata; swift_event_t rv = SWIFT_SUCCESS; void *buf = NULL; int nbytes = 0; if (cbd->dtmf) return SWIFT_INTERRUPTED; rv = swift_event_get_audio(event, &buf, &nbytes); if (rv == SWIFT_SUCCESS && buf && nbytes > 0) { int bytes_to_copy = nbytes; void *offset = buf; if (cbd->buflen + nbytes < sizeof(cbd->buf)) { memcpy(cbd->buf + cbd->buflen, buf, nbytes); cbd->buflen += nbytes; } else { /* This will probably never need to be executed, but better safe than sorry. */ fprintf(stderr, "This should never happen\n"); while (nbytes > sizeof(cbd->buf) - cbd->buflen) { bytes_to_copy = sizeof(cbd->buf) - cbd->buflen; memcpy(cbd->buf + cbd->buflen, buf, bytes_to_copy); cbd->buflen += bytes_to_copy; offset += bytes_to_copy; nbytes -= bytes_to_copy; queue_some_frames(cbd, 0); if (cbd->dtmf) break; } } queue_some_frames(cbd, 0); } return rv; } static int _speak_exec(struct ast_channel *chan, void *data, int wantdtmf) { char *textstring = data; swift_engine *engine; swift_voice *voice; swift_result_t rv; const char *tts_name; const char *tts_rate; int oldwriteformat; struct callback_data cbd = { .chan = chan, .wantdtmf = wantdtmf ? 1 : 0, }; if (!data) return 0; do { if (!(engine = swift_engine_open(NULL))) { ast_log(LOG_ERROR, "Unable to open Cepstral engine\n"); break; } if (!(cbd.port = swift_port_open(engine, NULL))) { ast_log(LOG_ERROR, "Unable to open Cepstral port\n"); swift_engine_close(engine); break; } if ((tts_name = pbx_builtin_getvar_helper(chan, "~SPEAKVOICE"))) { if (!(voice = swift_port_set_voice_by_name(cbd.port, tts_name))) { ast_log(LOG_ERROR, "Unable to find %s voice in Cepstral engine\n", tts_name); /* Fallback to any voice */ goto findanyvoice; } } else { findanyvoice: if ((voice = swift_port_find_first_voice(cbd.port, NULL, NULL)) == NULL) { ast_log(LOG_ERROR, "Unable to find any voice in Cepstral engine\n"); break; } if ((rv = swift_port_set_voice(cbd.port, voice)) != SWIFT_SUCCESS) { ast_log(LOG_ERROR, "Unable to set Cepstral voice %p: %s\n", voice, swift_strerror(rv)); while ((voice = swift_port_find_next_voice(cbd.port))) { if ((rv = swift_port_set_voice(cbd.port, voice)) != SWIFT_SUCCESS) ast_log(LOG_ERROR, "Unable to set Cepstral voice %p: %s\n", voice, swift_strerror(rv)); else break; } } } /* We use the voice rate to determine whether or not we need to downsample */ tts_rate = swift_voice_get_attribute(voice, "sample-rate"); sscanf(tts_rate, "%hd", &cbd.voice_rate); swift_port_set_callback(cbd.port, &audio_callback, SWIFT_EVENT_AUDIO, &cbd); if (chan->_state != AST_STATE_UP) { if (ast_answer(chan)) break; } /* We queue in slinear */ oldwriteformat = chan->writeformat; ast_set_write_format(chan, AST_FORMAT_SLINEAR); /* This needs to occur asynchronously, otherwise we can't cancel it early. */ rv = swift_port_speak_text(cbd.port, textstring, 0, NULL, &cbd.tid, NULL); if (SWIFT_FAILED(rv)) { ast_log(LOG_ERROR, "Unable to speak prompt: %s\n", swift_strerror(rv)); break; } swift_port_wait(cbd.port, cbd.tid); } while (0); if (oldwriteformat) ast_set_write_format(chan, oldwriteformat); if (cbd.port != NULL) swift_port_close(cbd.port); if (engine != NULL) swift_engine_close(engine); return cbd.dtmf; } static int speak_exec(struct ast_channel *chan, void *data) { return _speak_exec(chan, data, 0); } static int speak2_exec(struct ast_channel *chan, void *data) { return _speak_exec(chan, data, 1); } static int speakvoice_read(struct ast_channel *chan, char *cmd, char *data, char *buf, size_t len) { const char *value = pbx_builtin_getvar_helper(chan, "~SPEAKVOICE"); if (value) ast_copy_string(buf, value, len); else { swift_engine *engine = swift_engine_open(NULL); swift_port *port; swift_voice *voice; if (!engine) return -1; if (!(port = swift_port_open(engine, NULL))) { swift_engine_close(engine); return -1; } if (!(voice = swift_port_find_first_voice(port, NULL, NULL))) { swift_port_close(port); swift_engine_close(engine); return -1; } if ((value = swift_voice_get_attribute(voice, "name"))) ast_copy_string(buf, value, len); else buf[0] = '\0'; swift_port_close(port); swift_engine_close(engine); } return 0; } static int speakvoice_write(struct ast_channel *chan, char *cmd, char *data, const char *value) { swift_engine *engine = swift_engine_open(NULL); swift_port *port; swift_voice *voice; int res = 0; if (!engine) { ast_log(LOG_ERROR, "Could not start the Speak engine\n"); return -1; } if (!(port = swift_port_open(engine, NULL))) { ast_log(LOG_ERROR, "Could not start the Speak engine\n"); swift_engine_close(engine); return -1; } if ((voice = swift_port_set_voice_by_name(port, data))) { pbx_builtin_setvar_helper(chan, "__~SPEAKVOICE", data); } else { ast_log(LOG_WARNING, "Could not find the '%s' voice\n", data); res = -1; } swift_port_close(port); swift_engine_close(engine); return res; } static struct ast_custom_function speakvoice_function = { .name = "SPEAK_VOICE", .synopsis = "Gets or sets the current voice used with Speak or SpeakUntilExten", .syntax = "SPEAK_VOICE()", .desc = "Retrieves the current voice setting for invocations of Speak or\n" "SpeakUntilExten. May also be set.\n", .read = speakvoice_read, .write = speakvoice_write, }; static int speakvoices_read(struct ast_channel *chan, char *cmd, char *data, char *buf, size_t len) { swift_engine *engine = swift_engine_open(NULL); swift_port *port; swift_voice *voice; buf[0] = '\0'; if (!engine) return -1; if (!(port = swift_port_open(engine, NULL))) { swift_engine_close(engine); return -1; } for (voice = swift_port_find_first_voice(port, NULL, NULL); voice; voice = swift_port_find_next_voice(port)) { const char *name = swift_voice_get_attribute(voice, "name"); if (!ast_strlen_zero(buf)) ast_copy_string(buf + strlen(buf), ",", len); ast_copy_string(buf + strlen(buf), S_OR(name, ""), len); } swift_port_close(port); swift_engine_close(engine); return 0; } static struct ast_custom_function speakvoices_function = { .name = "SPEAK_VOICES", .synopsis = "Retrieves a comma-separated list of available voices", .syntax = "SPEAK_VOICES()", .desc = "Retrieves a comma-separated list of available voicees for invocations of\n" " Speak or SpeakUntilExten.\n", .read = speakvoices_read, }; static int unload_module(void) { int res; res = ast_unregister_application(app_speak); res |= ast_unregister_application(app_speak2); res |= ast_custom_function_unregister(&speakvoice_function); res |= ast_custom_function_unregister(&speakvoices_function); return res; } static int load_module(void) { int res; res = ast_register_application(app_speak, speak_exec, speak_synopsis, speak_descrip); res |= ast_register_application(app_speak2, speak2_exec, speak2_synopsis, speak2_descrip); res |= ast_custom_function_register(&speakvoice_function); res |= ast_custom_function_register(&speakvoices_function); return res; } AST_MODULE_INFO(ASTERISK_GPL_KEY, AST_MODFLAG_GLOBAL_SYMBOLS, "Speech synthesis", .load = load_module, .unload = unload_module, );