diff --git a/doc/CHANGES-staging/say.txt b/doc/CHANGES-staging/say.txt new file mode 100644 index 0000000000..115ceea15f --- /dev/null +++ b/doc/CHANGES-staging/say.txt @@ -0,0 +1,7 @@ +Subject: say.c + +Adds SAYFILES function to retrieve the file names that would +be played by corresponding Say applications, such as +SayDigits, SayAlpha, etc. + +Additionally adds SayMoney and SayOrdinal applications. diff --git a/funcs/func_sayfiles.c b/funcs/func_sayfiles.c new file mode 100644 index 0000000000..81f3259ad9 --- /dev/null +++ b/funcs/func_sayfiles.c @@ -0,0 +1,396 @@ +/* + * Asterisk -- An open source telephony toolkit. + * + * Copyright (C) 2021, Naveen Albert + * + * 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. + */ + +/*! \file + * + * \brief Returns files played by Say applications + * + * \author Naveen Albert + * \ingroup functions + */ + +/*** MODULEINFO + extended + ***/ + +#include "asterisk.h" + +#include "asterisk/pbx.h" +#include "asterisk/file.h" +#include "asterisk/channel.h" +#include "asterisk/say.h" +#include "asterisk/lock.h" +#include "asterisk/localtime.h" +#include "asterisk/utils.h" +#include "asterisk/app.h" +#include "asterisk/test.h" +#include "asterisk/module.h" +#include "asterisk/conversions.h" + +/*** DOCUMENTATION + + + Returns the ampersand-delimited file names that would be played by the Say applications (e.g. SayAlpha, SayDigits). + + + + The value to be translated to filenames. + + + Say application type. + + + Files played by SayAlpha(). Default if none is specified. + + + Files played by SayDigits(). + + + Files played by SayMoney(). Currently supported for English and US dollars only. + + + Files played by SayNumber(). Currently supported for English only. + + + Files played by SayOrdinal(). Currently supported for English only. + + + Files played by SayPhonetic(). + + + + + + Returns the files that would be played by a Say application. These filenames could then be + passed directly into Playback, BackGround, Read, Queue, or any application which supports + playback of multiple ampersand-delimited files. + + same => n,Read(response,${SAYFILES(123,number)}) + + + + SayAlpha + SayDigits + SayMoney + SayNumber + SayOrdinal + SayPhonetic + + + ***/ +static int sayfile_exec(struct ast_channel *chan, const char *cmd, char *data, char *buf, size_t len) +{ + char *value, *type, *files; + const char *lang; + struct ast_str *filenames = NULL; + AST_DECLARE_APP_ARGS(args, + AST_APP_ARG(value); + AST_APP_ARG(type); + ); + + if (ast_strlen_zero(data)) { + ast_log(LOG_WARNING, "SAYFILES requires an argument\n"); + return 0; + } + + AST_STANDARD_APP_ARGS(args, data); + + value = args.value; + type = (ast_strlen_zero(args.type) ? "alpha" : args.type); + lang = (chan ? ast_channel_language(chan) : "en"); /* No chan for unit tests */ + + if (!strcmp(type, "alpha")) { + filenames = ast_get_character_str(value, lang, AST_SAY_CASE_NONE); + } else if (!strcmp(type, "phonetic")) { + filenames = ast_get_phonetic_str(value, lang); + } else if (!strcmp(type, "digits")) { + filenames = ast_get_digit_str(value, lang); + } else if (!strcmp(type, "number")) { + int num; + if (ast_str_to_int(value, &num)) { + ast_log(LOG_WARNING, "Invalid numeric argument: %s\n", value); + } else { + filenames = ast_get_number_str(num, lang); + } + } else if (!strcmp(type, "ordinal")) { + int num; + if (ast_str_to_int(value, &num)) { + ast_log(LOG_WARNING, "Invalid numeric argument: %s\n", value); + } else { + filenames = ast_get_ordinal_str(num, lang); + } + } else if (!strcmp(type, "money")) { + filenames = ast_get_money_str(value, lang); + } else { + ast_log(LOG_WARNING, "Invalid say type specified: %s\n", type); + } + + if (!filenames) { + return -1; + } + + files = ast_str_buffer(filenames); + snprintf(buf, len, "%s", files); + ast_free(filenames); + + return 0; +} + +static struct ast_custom_function sayfiles = { + .name = "SAYFILES", + .read = sayfile_exec, +}; + +#ifdef TEST_FRAMEWORK +AST_TEST_DEFINE(test_SAYFILES_function) +{ + enum ast_test_result_state res = AST_TEST_PASS; + struct ast_str *expr, *result; + + switch (cmd) { + case TEST_INIT: + info->name = "test_SAYFILES_function"; + info->category = "/funcs/func_sayfiles/"; + info->summary = "Test SAYFILES function substitution"; + info->description = + "Executes a series of variable substitutions using the SAYFILES function and ensures that the expected results are received."; + return AST_TEST_NOT_RUN; + case TEST_EXECUTE: + break; + } + + ast_test_status_update(test, "Testing SAYFILES() substitution ...\n"); + + if (!(expr = ast_str_create(16))) { + return AST_TEST_FAIL; + } + if (!(result = ast_str_create(16))) { + ast_free(expr); + return AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(hi Th3re,alpha)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "letters/h&letters/i&letters/space&letters/t&letters/h&digits/3&letters/r&letters/e") != 0) { + ast_test_status_update(test, "SAYFILES(hi Th3re,alpha) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(phreak,phonetic)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "phonetic/p_p&phonetic/h_p&phonetic/r_p&phonetic/e_p&phonetic/a_p&phonetic/k_p") != 0) { + ast_test_status_update(test, "SAYFILES(phreak,phonetic) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(35,digits)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/3&digits/5") != 0) { + ast_test_status_update(test, "SAYFILES(35,digits) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(35,number)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/30&digits/5") != 0) { + ast_test_status_update(test, "SAYFILES(35,number) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(747,number)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/7&digits/hundred&digits/40&digits/7") != 0) { + ast_test_status_update(test, "SAYFILES(747,number) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(1042,number)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1&digits/thousand&digits/40&digits/2") != 0) { + ast_test_status_update(test, "SAYFILES(1042,number) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(0,number)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/0") != 0) { + ast_test_status_update(test, "SAYFILES(0,digits) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(2001000001,number)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/2&digits/billion&digits/1&digits/million&digits/1") != 0) { + ast_test_status_update(test, "SAYFILES(2001000001,number) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(7,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/h-7") != 0) { + ast_test_status_update(test, "SAYFILES(7,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(35,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/30&digits/h-5") != 0) { + ast_test_status_update(test, "SAYFILES(35,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(1042,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1&digits/thousand&digits/40&digits/h-2") != 0) { + ast_test_status_update(test, "SAYFILES(1042,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(11042,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/11&digits/thousand&digits/40&digits/h-2") != 0) { + ast_test_status_update(test, "SAYFILES(11042,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(40000,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/40&digits/h-thousand") != 0) { + ast_test_status_update(test, "SAYFILES(40000,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(43638,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/40&digits/3&digits/thousand&digits/6&digits/hundred&digits/30&digits/h-8") != 0) { + ast_test_status_update(test, "SAYFILES(43638,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(1000000,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1&digits/h-million") != 0) { + ast_test_status_update(test, "SAYFILES(1000000,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(1000001,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1&digits/million&digits/h-1") != 0) { + ast_test_status_update(test, "SAYFILES(1000001,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(2001000001,ordinal)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/2&digits/billion&digits/1&digits/million&digits/h-1") != 0) { + ast_test_status_update(test, "SAYFILES(2001000001,ordinal) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(0,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/0¢s") != 0) { + ast_test_status_update(test, "SAYFILES(0,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(0.01,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1¢") != 0) { + ast_test_status_update(test, "SAYFILES(0.01,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(0.42,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/40&digits/2¢s") != 0) { + ast_test_status_update(test, "SAYFILES(0.42,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(1.00,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1&letters/dollar") != 0) { + ast_test_status_update(test, "SAYFILES(1.00,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(1.42,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/1&letters/dollar_&and&digits/40&digits/2¢s") != 0) { + ast_test_status_update(test, "SAYFILES(1.42,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(2.00,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/2&dollars") != 0) { + ast_test_status_update(test, "SAYFILES(2.00,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_str_set(&expr, 0, "${SAYFILES(2.42,money)}"); + ast_str_substitute_variables(&result, 0, NULL, ast_str_buffer(expr)); + if (strcmp(ast_str_buffer(result), "digits/2&dollars&and&digits/40&digits/2¢s") != 0) { + ast_test_status_update(test, "SAYFILES(2.42,money) test failed ('%s')\n", + ast_str_buffer(result)); + res = AST_TEST_FAIL; + } + + ast_free(expr); + ast_free(result); + + return res; +} +#endif + +static int unload_module(void) +{ + AST_TEST_UNREGISTER(test_SAYFILES_function); + return ast_custom_function_unregister(&sayfiles); +} + +static int load_module(void) +{ + AST_TEST_REGISTER(test_SAYFILES_function); + return ast_custom_function_register(&sayfiles); +} + +AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Say application files"); diff --git a/include/asterisk/say.h b/include/asterisk/say.h index a4aa90cdf3..5e55b0f62c 100644 --- a/include/asterisk/say.h +++ b/include/asterisk/say.h @@ -85,6 +85,25 @@ int ast_say_number(struct ast_channel *chan, int num, /*! \brief Same as \ref ast_say_number() with audiofd for received audio and returns 1 on ctrlfd being readable */ SAY_EXTERN int (* ast_say_number_full)(struct ast_channel *chan, int num, const char *ints, const char *lang, const char *options, int audiofd, int ctrlfd) SAY_INIT(ast_say_number_full); +/*! + * \brief says an ordinal number + * \param chan channel to say them number on + * \param num ordinal number to say on the channel + * \param ints which dtmf to interrupt on + * \param lang language to speak the number + * \param options set to 'f' for female, 'm' for male, 'c' for commune, 'n' for neuter + * \details + * Vocally says an ordinal number on a given channel + * \retval 0 on success + * \retval DTMF digit on interrupt + * \retval -1 on failure + */ +int ast_say_ordinal(struct ast_channel *chan, int num, + const char *ints, const char *lang, const char *options); + +/*! \brief Same as \ref ast_say_number() with audiofd for received audio and returns 1 on ctrlfd being readable */ +SAY_EXTERN int (* ast_say_ordinal_full)(struct ast_channel *chan, int num, const char *ints, const char *lang, const char *options, int audiofd, int ctrlfd) SAY_INIT(ast_say_ordinal_full); + /*! * \brief says an enumeration * \param chan channel to say them enumeration on @@ -142,6 +161,14 @@ int ast_say_digit_str(struct ast_channel *chan, const char *num, /*! \brief Same as \ref ast_say_digit_str() with audiofd for received audio and returns 1 on ctrlfd being readable */ SAY_EXTERN int (* ast_say_digit_str_full)(struct ast_channel *chan, const char *num, const char *ints, const char *lang, int audiofd, int ctrlfd) SAY_INIT(ast_say_digit_str_full); +/*! \brief + * function to pronounce monetary amounts + */ +int ast_say_money_str(struct ast_channel *chan, const char *num, + const char *ints, const char *lang); + +SAY_EXTERN int (* ast_say_money_str_full)(struct ast_channel *chan, const char *num, const char *ints, const char *lang, int audiofd, int ctrlfd) SAY_INIT(ast_say_money_str_full); + /*! \brief * the generic 'say' routine, with the first chars in the string * defining the format to use @@ -184,6 +211,79 @@ int ast_say_counted_noun(struct ast_channel *chan, int num, const char *noun); int ast_say_counted_adjective(struct ast_channel *chan, int num, const char *adjective, const char *gender); +/*! + * \brief Returns an ast_str of files for SayAlpha playback. + * + * \param str Text to be translated to the corresponding audio files. + * \param lang Channel language + * \param sensitivity Case sensitivity + * + * Computes the list of files to be played by SayAlpha. + * + * \retval ampersand-separated string of Asterisk sound files that can be played back. + */ +struct ast_str* ast_get_character_str(const char *str, const char *lang, enum ast_say_case_sensitivity sensitivity); + +/*! + * \brief Returns an ast_str of files for SayPhonetic playback. + * + * \param str Text to be translated to the corresponding audio files. + * \param lang Channel language + * + * Computes the list of files to be played by SayPhonetic. + * + * \retval ampersand-separated string of Asterisk sound files that can be played back. + */ +struct ast_str* ast_get_phonetic_str(const char *str, const char *lang); + +/*! + * \brief Returns an ast_str of files for SayDigits playback. + * + * \param str Text to be translated to the corresponding audio files. + * \param lang Channel language + * + * Computes the list of files to be played by SayDigits. + * + * \retval ampersand-separated string of Asterisk sound files that can be played back. + */ +struct ast_str* ast_get_digit_str(const char *str, const char *lang); + +/*! + * \brief Returns an ast_str of files for SayMoney playback. + * + * \param str Text to be translated to the corresponding audio files. + * \param lang Channel language + * + * Computes the list of files to be played by SayMoney. + * + * \retval ampersand-separated string of Asterisk sound files that can be played back. + */ +struct ast_str* ast_get_money_str(const char *str, const char *lang); + +/*! + * \brief Returns an ast_str of files for SayNumber playback. + * + * \param num Integer to be translated to the corresponding audio files. + * \param lang Channel language + * + * Computes the list of files to be played by SayNumber. + * + * \retval ampersand-separated string of Asterisk sound files that can be played back. + */ +struct ast_str* ast_get_number_str(int num, const char *lang); + +/*! + * \brief Returns an ast_str of files for SayOrdinal playback. + * + * \param num Integer to be translated to the corresponding audio files. + * \param lang Channel language + * + * Computes the list of files to be played by SayOrdinal. + * + * \retval ampersand-separated string of Asterisk sound files that can be played back. + */ +struct ast_str* ast_get_ordinal_str(int num, const char *lang); + #if defined(__cplusplus) || defined(c_plusplus) } #endif diff --git a/main/channel.c b/main/channel.c index 9e33fb25fc..9104ef8ca1 100644 --- a/main/channel.c +++ b/main/channel.c @@ -8257,6 +8257,12 @@ int ast_say_number(struct ast_channel *chan, int num, return ast_say_number_full(chan, num, ints, language, options, -1, -1); } +int ast_say_ordinal(struct ast_channel *chan, int num, + const char *ints, const char *language, const char *options) +{ + return ast_say_ordinal_full(chan, num, ints, language, options, -1, -1); +} + int ast_say_enumeration(struct ast_channel *chan, int num, const char *ints, const char *language, const char *options) { @@ -8275,6 +8281,12 @@ int ast_say_digit_str(struct ast_channel *chan, const char *str, return ast_say_digit_str_full(chan, str, ints, lang, -1, -1); } +int ast_say_money_str(struct ast_channel *chan, const char *str, + const char *ints, const char *lang) +{ + return ast_say_money_str_full(chan, str, ints, lang, -1, -1); +} + int ast_say_character_str(struct ast_channel *chan, const char *str, const char *ints, const char *lang, enum ast_say_case_sensitivity sensitivity) { diff --git a/main/pbx_builtins.c b/main/pbx_builtins.c index e3703382f7..eae4c6ba8f 100644 --- a/main/pbx_builtins.c +++ b/main/pbx_builtins.c @@ -37,6 +37,7 @@ #include "asterisk/say.h" #include "asterisk/app.h" #include "asterisk/module.h" +#include "asterisk/conversions.h" #include "pbx_private.h" /*** DOCUMENTATION @@ -440,9 +441,12 @@ SayDigits + SayMoney SayNumber + SayOrdinal SayPhonetic CHANNEL + SAYFILES @@ -481,7 +485,9 @@ SayDigits + SayMoney SayNumber + SayOrdinal SayPhonetic SayAlpha CHANNEL @@ -503,9 +509,35 @@ SayAlpha + SayMoney SayNumber + SayOrdinal SayPhonetic CHANNEL + SAYFILES + + + + + Say Money. + + + + + + This application will play the currency sounds for the given floating point number + in the current language. Currently only English and US Dollars is supported. + If the channel variable SAY_DTMF_INTERRUPT is set to 'true' + (case insensitive), then this application will react to DTMF in the same way as + Background. + + + SayAlpha + SayNumber + SayOrdinal + SayPhonetic + CHANNEL + SAYFILES @@ -527,8 +559,37 @@ SayAlpha SayDigits + SayMoney + SayPhonetic + CHANNEL + SAYFILES + + + + + Say Ordinal Number. + + + + + + + This application will play the ordinal sounds that correspond to the given + digits (e.g. 1st, 42nd). Currently only English is supported. + Optionally, a gender may be + specified. This will use the language that is currently set for the channel. See the CHANNEL() + function for more information on setting the language for the channel. If the channel variable + SAY_DTMF_INTERRUPT is set to 'true' (case insensitive), then this + application will react to DTMF in the same way as Background. + + + SayAlpha + SayDigits + SayMoney + SayNumber SayPhonetic CHANNEL + SAYFILES @@ -547,7 +608,10 @@ SayAlpha SayDigits + SayMoney SayNumber + SayOrdinal + SAYFILES @@ -1283,7 +1347,7 @@ static int pbx_builtin_saynumber(struct ast_channel *chan, const char *data) ast_copy_string(tmp, data, sizeof(tmp)); strsep(&number, ","); - if (sscanf(tmp, "%d", &number_val) != 1) { + if (ast_str_to_int(tmp, &number_val)) { ast_log(LOG_WARNING, "argument '%s' to SayNumber could not be parsed as a number.\n", tmp); return 0; } @@ -1306,6 +1370,53 @@ static int pbx_builtin_saynumber(struct ast_channel *chan, const char *data) return interrupt ? res : 0; } +static int pbx_builtin_sayordinal(struct ast_channel *chan, const char *data) +{ + char tmp[256]; + char *number = tmp; + int number_val; + char *options; + int res; + int interrupt = 0; + const char *interrupt_string; + + ast_channel_lock(chan); + interrupt_string = pbx_builtin_getvar_helper(chan, "SAY_DTMF_INTERRUPT"); + if (ast_true(interrupt_string)) { + interrupt = 1; + } + ast_channel_unlock(chan); + + if (ast_strlen_zero(data)) { + ast_log(LOG_WARNING, "SayOrdinal requires an argument (number)\n"); + return -1; + } + ast_copy_string(tmp, data, sizeof(tmp)); + strsep(&number, ","); + + if (ast_str_to_int(tmp, &number_val)) { + ast_log(LOG_WARNING, "argument '%s' to SayOrdinal could not be parsed as a number.\n", tmp); + return 0; + } + + options = strsep(&number, ","); + if (options) { + if ( strcasecmp(options, "f") && strcasecmp(options, "m") && + strcasecmp(options, "c") && strcasecmp(options, "n") ) { + ast_log(LOG_WARNING, "SayOrdinal gender option is either 'f', 'm', 'c' or 'n'\n"); + return -1; + } + } + + res = ast_say_ordinal(chan, number_val, interrupt ? AST_DIGIT_ANY : "", ast_channel_language(chan), options); + + if (res < 0 && !ast_check_hangup_locked(chan)) { + ast_log(LOG_WARNING, "We were unable to say the number %s, is it too large?\n", tmp); + } + + return interrupt ? res : 0; +} + static int pbx_builtin_saydigits(struct ast_channel *chan, const char *data) { int res = 0; @@ -1326,6 +1437,26 @@ static int pbx_builtin_saydigits(struct ast_channel *chan, const char *data) return res; } +static int pbx_builtin_saymoney(struct ast_channel *chan, const char *data) +{ + int res = 0; + int interrupt = 0; + const char *interrupt_string; + + ast_channel_lock(chan); + interrupt_string = pbx_builtin_getvar_helper(chan, "SAY_DTMF_INTERRUPT"); + if (ast_true(interrupt_string)) { + interrupt = 1; + } + ast_channel_unlock(chan); + + if (data) { + res = ast_say_money_str(chan, data, interrupt ? AST_DIGIT_ANY : "", ast_channel_language(chan)); + } + + return res; +} + static int pbx_builtin_saycharacters_case(struct ast_channel *chan, const char *data) { int res = 0; @@ -1483,7 +1614,9 @@ struct pbx_builtin { { "SayAlpha", pbx_builtin_saycharacters }, { "SayAlphaCase", pbx_builtin_saycharacters_case }, { "SayDigits", pbx_builtin_saydigits }, + { "SayMoney", pbx_builtin_saymoney }, { "SayNumber", pbx_builtin_saynumber }, + { "SayOrdinal", pbx_builtin_sayordinal }, { "SayPhonetic", pbx_builtin_sayphonetic }, { "SetAMAFlags", pbx_builtin_setamaflags }, { "Wait", pbx_builtin_wait }, diff --git a/main/say.c b/main/say.c index 0a37091ddb..009ee8f706 100644 --- a/main/say.c +++ b/main/say.c @@ -29,6 +29,8 @@ * Next Generation Networks (NGN). * \note 2007-03-20 : Support for Thai added by Dome C. , * IP Crossing Co., Ltd. + * \note 2021-07-26 : Refactoring to separate string buildup and playback + * by Naveen Albert */ /*** MODULEINFO @@ -58,9 +60,7 @@ /* Forward declaration */ static int wait_file(struct ast_channel *chan, const char *ints, const char *file, const char *lang); - -static int say_character_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, enum ast_say_case_sensitivity sensitivity, int audiofd, int ctrlfd) -{ +struct ast_str* ast_get_character_str(const char *str, const char *lang, enum ast_say_case_sensitivity sensitivity) { const char *fn; char fnbuf[10], asciibuf[20] = "letters/ascii"; char ltr; @@ -69,6 +69,12 @@ static int say_character_str_full(struct ast_channel *chan, const char *str, con int upper = 0; int lower = 0; + struct ast_str *filenames = ast_str_create(20); + if (!filenames) { + return NULL; + } + ast_str_reset(filenames); + while (str[num] && !res) { fn = NULL; switch (str[num]) { @@ -154,14 +160,7 @@ static int say_character_str_full(struct ast_channel *chan, const char *str, con } if ((fn && ast_fileexists(fn, NULL, lang) > 0) || (snprintf(asciibuf + 13, sizeof(asciibuf) - 13, "%d", str[num]) > 0 && ast_fileexists(asciibuf, NULL, lang) > 0 && (fn = asciibuf))) { - res = ast_streamfile(chan, fn, lang); - if (!res) { - if ((audiofd > -1) && (ctrlfd > -1)) - res = ast_waitstream_full(chan, ints, audiofd, ctrlfd); - else - res = ast_waitstream(chan, ints); - } - ast_stopstream(chan); + ast_str_append(&filenames, 0, (num == 0 ? "%s" : "&%s"), fn); } if (upper || lower) { continue; @@ -169,18 +168,56 @@ static int say_character_str_full(struct ast_channel *chan, const char *str, con num++; } + return filenames; +} + +static int say_filenames(struct ast_channel *chan, const char *ints, const char *lang, int audiofd, int ctrlfd, struct ast_str *filenames) +{ + int res = 0; + char *files; + const char *fn; + + if (!filenames) { + return -1; + } + files = ast_str_buffer(filenames); + + while ((fn = strsep(&files, "&"))) { + res = ast_streamfile(chan, fn, lang); + if (!res) { + if ((audiofd > -1) && (ctrlfd > -1)) + res = ast_waitstream_full(chan, ints, audiofd, ctrlfd); + else + res = ast_waitstream(chan, ints); + } + ast_stopstream(chan); + } + + ast_free(filenames); + return res; } -static int say_phonetic_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, int audiofd, int ctrlfd) +static int say_character_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, enum ast_say_case_sensitivity sensitivity, int audiofd, int ctrlfd) +{ + struct ast_str *filenames = ast_get_character_str(str, lang, sensitivity); + return say_filenames(chan, ints, lang, audiofd, ctrlfd, filenames); +} + +struct ast_str* ast_get_phonetic_str(const char *str, const char *lang) { const char *fn; char fnbuf[256]; char ltr; int num = 0; - int res = 0; - while (str[num] && !res) { + struct ast_str *filenames = ast_str_create(20); + if (!filenames) { + return NULL; + } + ast_str_reset(filenames); + + while (str[num]) { fn = NULL; switch (str[num]) { case ('*'): @@ -237,29 +274,33 @@ static int say_phonetic_str_full(struct ast_channel *chan, const char *str, cons fn = fnbuf; } if (fn && ast_fileexists(fn, NULL, lang) > 0) { - res = ast_streamfile(chan, fn, lang); - if (!res) { - if ((audiofd > -1) && (ctrlfd > -1)) - res = ast_waitstream_full(chan, ints, audiofd, ctrlfd); - else - res = ast_waitstream(chan, ints); - } - ast_stopstream(chan); + ast_str_append(&filenames, 0, (num == 0 ? "%s" : "&%s"), fn); } num++; } - return res; + return filenames; } -static int say_digit_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, int audiofd, int ctrlfd) +static int say_phonetic_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, int audiofd, int ctrlfd) +{ + struct ast_str *filenames = ast_get_phonetic_str(str, lang); + return say_filenames(chan, ints, lang, audiofd, ctrlfd, filenames); +} + +struct ast_str* ast_get_digit_str(const char *str, const char *lang) { const char *fn; char fnbuf[256]; int num = 0; - int res = 0; - while (str[num] && !res) { + struct ast_str *filenames = ast_str_create(20); + if (!filenames) { + return NULL; + } + ast_str_reset(filenames); + + while (str[num]) { fn = NULL; switch (str[num]) { case ('*'): @@ -287,19 +328,340 @@ static int say_digit_str_full(struct ast_channel *chan, const char *str, const c break; } if (fn && ast_fileexists(fn, NULL, lang) > 0) { - res = ast_streamfile(chan, fn, lang); - if (!res) { - if ((audiofd > -1) && (ctrlfd > -1)) - res = ast_waitstream_full(chan, ints, audiofd, ctrlfd); - else - res = ast_waitstream(chan, ints); - } - ast_stopstream(chan); + ast_str_append(&filenames, 0, (num == 0 ? "%s" : "&%s"), fn); } num++; } - return res; + return filenames; +} + +static int say_digit_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, int audiofd, int ctrlfd) +{ + struct ast_str *filenames = ast_get_digit_str(str, lang); + return say_filenames(chan, ints, lang, audiofd, ctrlfd, filenames); +} + +static struct ast_str* ast_get_money_en_dollars_str(const char *str, const char *lang) +{ + const char *fnr; + + double dollars = 0; + int amt, cents; + struct ast_str *fnrecurse = NULL; + + struct ast_str *filenames = ast_str_create(20); + if (!filenames) { + return NULL; + } + ast_str_reset(filenames); + + if (sscanf(str, "%30lf", &dollars) != 1) { + amt = 0; + } else { /* convert everything to cents */ + amt = dollars * 100; + } + + /* Just the cents after the dollar decimal point */ + cents = amt - (((int) dollars) * 100); + ast_debug(1, "Cents is %d, amount is %d\n", cents, amt); + + if (amt >= 100) { + fnrecurse = ast_get_number_str((amt / 100), lang); + if (!fnrecurse) { + ast_log(LOG_WARNING, "Couldn't get string for dollars\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, "%s", fnr); + } + + /* If this is it, end on a down pitch, otherwise up pitch */ + if (amt < 200) { + ast_str_append(&filenames, 0, "&%s", (cents > 0) ? "letters/dollar_" : "letters/dollar"); + } else { + ast_str_append(&filenames, 0, "&%s", "dollars"); + } + + /* If dollars and cents, add "and" in the middle */ + if (cents > 0) { + ast_str_append(&filenames, 0, "&%s", "and"); + } + } + + if (cents > 0) { + fnrecurse = ast_get_number_str(cents, lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for cents\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (amt < 100 ? "%s" : "&%s"), fnr); + } + ast_str_append(&filenames, 0, "&%s", (cents == 1) ? "cent" : "cents"); + } else if (amt == 0) { + fnrecurse = ast_get_digit_str("0", lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for cents\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, "%s", fnr); + } + ast_str_append(&filenames, 0, "&%s", "cents"); + } + + if (fnrecurse) { + ast_free(fnrecurse); + } + + return filenames; +} + +/*! \brief ast_get_money_str: call language-specific functions */ +struct ast_str* ast_get_money_str(const char *str, const char *lang) +{ + if (!strncasecmp(lang, "en", 2)) { /* English syntax */ + return ast_get_money_en_dollars_str(str, lang); + } + + ast_log(LOG_WARNING, "Language %s not currently supported, defaulting to US Dollars\n", lang); + /* Default to english */ + return ast_get_money_en_dollars_str(str, lang); +} + +static int say_money_str_full(struct ast_channel *chan, const char *str, const char *ints, const char *lang, int audiofd, int ctrlfd) +{ + struct ast_str *filenames = ast_get_money_str(str, lang); + return say_filenames(chan, ints, lang, audiofd, ctrlfd, filenames); +} + +static struct ast_str* get_number_str_en(int num, const char *lang) +{ + const char *fnr; + int loops = 0; + + int res = 0; + int playh = 0; + char fn[256] = ""; + + struct ast_str *filenames; + + if (!num) { + return ast_get_digit_str("0", lang); + } + + filenames = ast_str_create(20); + if (!filenames) { + return NULL; + } + ast_str_reset(filenames); + + while (!res && (num || playh)) { + if (num < 0) { + ast_copy_string(fn, "digits/minus", sizeof(fn)); + if ( num > INT_MIN ) { + num = -num; + } else { + num = 0; + } + } else if (playh) { + ast_copy_string(fn, "digits/hundred", sizeof(fn)); + playh = 0; + } else if (num < 20) { + snprintf(fn, sizeof(fn), "digits/%d", num); + num = 0; + } else if (num < 100) { + snprintf(fn, sizeof(fn), "digits/%d", (num /10) * 10); + num %= 10; + } else { + if (num < 1000){ + snprintf(fn, sizeof(fn), "digits/%d", (num/100)); + playh++; + num %= 100; + } else { + struct ast_str *fnrecurse = NULL; + if (num < 1000000) { /* 1,000,000 */ + fnrecurse = get_number_str_en((num / 1000), lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for num\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fnr); + } + num %= 1000; + snprintf(fn, sizeof(fn), "&digits/thousand"); + } else { + if (num < 1000000000) { /* 1,000,000,000 */ + fnrecurse = get_number_str_en((num / 1000000), lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for num\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fnr); + } + num %= 1000000; + ast_copy_string(fn, "&digits/million", sizeof(fn)); + } else { + if (num < INT_MAX) { + fnrecurse = get_number_str_en((num / 1000000000), lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for num\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fnr); + } + num %= 1000000000; + ast_copy_string(fn, "&digits/billion", sizeof(fn)); + } else { + ast_log(LOG_WARNING, "Number '%d' is too big for me\n", num); + res = -1; + } + } + } + if (fnrecurse) { + ast_free(fnrecurse); + } + /* we already decided whether or not to add an &, don't add another one immediately */ + loops = 0; + } + } + if (!res) { + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fn); + loops++; + } + } + + return filenames; +} + +/*! \brief ast_get_number_str: call language-specific functions */ +struct ast_str* ast_get_number_str(int num, const char *lang) +{ + if (!strncasecmp(lang, "en", 2)) { /* English syntax */ + return get_number_str_en(num, lang); + } + + ast_log(LOG_WARNING, "Language %s not currently supported, defaulting to English\n", lang); + /* Default to english */ + return get_number_str_en(num, lang); +} + +static struct ast_str* get_ordinal_str_en(int num, const char *lang) +{ + const char *fnr; + int loops = 0; + + int res = 0; + int playh = 0; + char fn[256] = ""; + + struct ast_str *filenames; + + if (!num) { + num = 0; + } + + filenames = ast_str_create(20); + if (!filenames) { + return NULL; + } + ast_str_reset(filenames); + + while (!res && (num || playh)) { + if (num < 0) { + ast_copy_string(fn, "digits/minus", sizeof(fn)); + if ( num > INT_MIN ) { + num = -num; + } else { + num = 0; + } + } else if (playh) { + ast_copy_string(fn, (num % 100 == 0) ? "digits/h-hundred" : "digits/hundred", sizeof(fn)); + playh = 0; + } else if (num < 20) { + if (num > 0) { + snprintf(fn, sizeof(fn), "digits/h-%d", num); + } else { + ast_log(LOG_ERROR, "Unsupported ordinal number: %d\n", num); + } + num = 0; + } else if (num < 100) { + int base = (num / 10) * 10; + if (base != num) { + snprintf(fn, sizeof(fn), "digits/%d", base); + } else { + snprintf(fn, sizeof(fn), "digits/h-%d", base); + } + num %= 10; + } else { + if (num < 1000){ + snprintf(fn, sizeof(fn), "digits/%d", (num/100)); + playh++; + num %= 100; + } else { + struct ast_str *fnrecurse = NULL; + if (num < 1000000) { /* 1,000,000 */ + fnrecurse = get_number_str_en((num / 1000), lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for num\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fnr); + } + num %= 1000; + snprintf(fn, sizeof(fn), (num % 1000 == 0) ? "&digits/h-thousand" : "&digits/thousand"); + } else { + if (num < 1000000000) { /* 1,000,000,000 */ + fnrecurse = get_number_str_en((num / 1000000), lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for num\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fnr); + } + num %= 1000000; + ast_copy_string(fn, (num % 1000000 == 0) ? "&digits/h-million" : "&digits/million", sizeof(fn)); + } else { + if (num < INT_MAX) { + fnrecurse = get_number_str_en((num / 1000000000), lang); + if (!fnrecurse) { + ast_log(LOG_ERROR, "Couldn't get string for num\n"); + } else { + fnr = ast_str_buffer(fnrecurse); + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fnr); + } + num %= 1000000000; + ast_copy_string(fn, (num % 1000000000 == 0) ? "&digits/h-billion" : "&digits/billion", sizeof(fn)); + } else { + ast_log(LOG_WARNING, "Number '%d' is too big for me\n", num); + res = -1; + } + } + } + if (fnrecurse) { + ast_free(fnrecurse); + } + /* we already decided whether or not to add an &, don't add another one immediately */ + loops = 0; + } + } + if (!res) { + ast_str_append(&filenames, 0, (loops == 0 ? "%s" : "&%s"), fn); + loops++; + } + } + + return filenames; +} + +/*! \brief ast_get_ordinal_str: call language-specific functions */ +struct ast_str* ast_get_ordinal_str(int num, const char *lang) +{ + if (!strncasecmp(lang, "en", 2)) { /* English syntax */ + return get_ordinal_str_en(num, lang); + } + + ast_log(LOG_WARNING, "Language %s not currently supported, defaulting to English\n", lang); + /* Default to english */ + return get_ordinal_str_en(num, lang); } /* Forward declarations */ @@ -540,66 +902,15 @@ static int say_number_full(struct ast_channel *chan, int num, const char *ints, \note This is the default syntax, if no other syntax defined in this file is used */ static int ast_say_number_full_en(struct ast_channel *chan, int num, const char *ints, const char *language, int audiofd, int ctrlfd) { - int res = 0; - int playh = 0; - char fn[256] = ""; - if (!num) - return ast_say_digits_full(chan, 0, ints, language, audiofd, ctrlfd); + struct ast_str *filenames = ast_get_number_str(num, language); + return say_filenames(chan, ints, language, audiofd, ctrlfd, filenames); +} - while (!res && (num || playh)) { - if (num < 0) { - ast_copy_string(fn, "digits/minus", sizeof(fn)); - if ( num > INT_MIN ) { - num = -num; - } else { - num = 0; - } - } else if (playh) { - ast_copy_string(fn, "digits/hundred", sizeof(fn)); - playh = 0; - } else if (num < 20) { - snprintf(fn, sizeof(fn), "digits/%d", num); - num = 0; - } else if (num < 100) { - snprintf(fn, sizeof(fn), "digits/%d", (num /10) * 10); - num %= 10; - } else { - if (num < 1000){ - snprintf(fn, sizeof(fn), "digits/%d", (num/100)); - playh++; - num %= 100; - } else { - if (num < 1000000) { /* 1,000,000 */ - res = ast_say_number_full_en(chan, num / 1000, ints, language, audiofd, ctrlfd); - if (res) - return res; - num %= 1000; - snprintf(fn, sizeof(fn), "digits/thousand"); - } else { - if (num < 1000000000) { /* 1,000,000,000 */ - res = ast_say_number_full_en(chan, num / 1000000, ints, language, audiofd, ctrlfd); - if (res) - return res; - num %= 1000000; - ast_copy_string(fn, "digits/million", sizeof(fn)); - } else { - ast_debug(1, "Number '%d' is too big for me\n", num); - res = -1; - } - } - } - } - if (!res) { - if (!ast_streamfile(chan, fn, language)) { - if ((audiofd > -1) && (ctrlfd > -1)) - res = ast_waitstream_full(chan, ints, audiofd, ctrlfd); - else - res = ast_waitstream(chan, ints); - } - ast_stopstream(chan); - } - } - return res; +/*! \brief say_ordinal_full */ +static int say_ordinal_full(struct ast_channel *chan, int num, const char *ints, const char *language, const char *options, int audiofd, int ctrlfd) +{ + struct ast_str *filenames = ast_get_ordinal_str(num, language); + return say_filenames(chan, ints, language, audiofd, ctrlfd, filenames); } static int exp10_int(int power) @@ -9410,8 +9721,10 @@ int ast_say_counted_adjective(struct ast_channel *chan, int num, const char adje static void __attribute__((constructor)) __say_init(void) { ast_say_number_full = say_number_full; + ast_say_ordinal_full = say_ordinal_full; ast_say_enumeration_full = say_enumeration_full; ast_say_digit_str_full = say_digit_str_full; + ast_say_money_str_full = say_money_str_full; ast_say_character_str_full = say_character_str_full; ast_say_phonetic_str_full = say_phonetic_str_full; ast_say_datetime = say_datetime;