| |
| /* |
| * espeak.c - Speech Dispatcher backend for espeak |
| * |
| * Copyright (C) 2007 Brailcom, o.p.s. |
| * Copyright (C) 2019-2025 Samuel Thibault <samuel.thibault@ens-lyon.org> |
| * |
| * This is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2, or (at your option) |
| * any later version. |
| * |
| * This software is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program. If not, see <https://www.gnu.org/licenses/>. |
| * |
| * @author Lukas Loehrer |
| * Based on ibmtts.c. |
| * |
| * $Id: espeak.c,v 1.11 2008-10-15 17:04:36 hanke Exp $ |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include <config.h> |
| #endif |
| |
| /* < Includes*/ |
| |
| /* System includes. */ |
| #include <string.h> |
| #include <ctype.h> |
| #include <glib.h> |
| #include <fcntl.h> |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| #ifdef __linux__ |
| #include <sys/inotify.h> |
| #endif |
| #endif |
| |
| /* espeak header file */ |
| #ifdef ESPEAK_NG_INCLUDE |
| #include <espeak-ng/espeak_ng.h> |
| #else |
| #include <espeak/speak_lib.h> |
| #endif |
| |
| #ifndef ESPEAK_API_REVISION |
| #define ESPEAK_API_REVISION 1 |
| #endif |
| |
| /* Speech Dispatcher includes. */ |
| #include "spd_audio.h" |
| #include <speechd_types.h> |
| #include "module_utils.h" |
| |
| /* > */ |
| /* < Basic definitions*/ |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| #define MODULE_NAME "espeak-ng" |
| #define DBG_MODNAME "Espeak-ng:" |
| #else |
| #define MODULE_NAME "espeak" |
| #define DBG_MODNAME "Espeak:" |
| #endif |
| |
| #define MODULE_VERSION "0.1" |
| |
| #define DEBUG_MODULE 1 |
| DECLARE_DEBUG() |
| #define DBG_WARN(e, msg) do { \ |
| if (Debug && !(e)) { \ |
| DBG(DBG_MODNAME " Warning: " msg); \ |
| } \ |
| } while (0) |
| typedef enum { |
| FATAL_ERROR = -1, |
| OK = 0, |
| ERROR = 1 |
| } TEspeakSuccess; |
| |
| static int espeak_sample_rate = 0; |
| static SPDVoice **espeak_voice_list = NULL; |
| #ifdef ESPEAK_NG_INCLUDE |
| #ifdef __linux__ |
| static int mbrola_voice_inotify = -1; |
| #endif |
| #endif |
| #ifdef ESPEAK_NG_INCLUDE |
| struct espeak_variant { |
| char *name; |
| char *identifier; |
| }; |
| static struct espeak_variant *espeak_variants_array = NULL; |
| #endif |
| |
| /* When a voice is set, this is the baseline pitch of the voice. |
| SSIP PITCH commands then adjust relative to this. */ |
| static int espeak_voice_pitch_baseline = 50; |
| |
| /* When a voice is set, this is the baseline pitch range of the voice. |
| SSIP PITCH range commands then adjust relative to this. */ |
| static int espeak_voice_pitch_range_baseline = 50; |
| |
| static int stop_requested = 0; |
| static int pause_requested = 0; |
| static int pause_index_sent = 0; |
| static int began = 0; |
| |
| static gboolean initialized = FALSE; |
| |
| /* <Function prototypes*/ |
| |
| static TEspeakSuccess espeak_set_punctuation_list_from_utf8(const char *punct); |
| static SPDVoice **espeak_list_synthesis_voices(); |
| static void espeak_free_voice_list(); |
| |
| /* Callbacks */ |
| static int synth_callback(short *wav, int numsamples, espeak_EVENT * events); |
| static int uri_callback(int type, const char *uri, const char *base); |
| |
| /* Internal function prototypes for main thread. */ |
| |
| /* Basic parameters */ |
| static void espeak_set_rate(signed int rate); |
| static void espeak_set_pitch(signed int pitch); |
| static void espeak_set_pitch_range(signed int pitch_range); |
| static void espeak_set_volume(signed int volume); |
| static void espeak_set_punctuation_mode(SPDPunctuation punct_mode); |
| static void espeak_set_cap_let_recogn(SPDCapitalLetters cap_mode); |
| |
| /* Voices and languages */ |
| static void espeak_set_language(char *lang); |
| static void espeak_set_voice(SPDVoiceType voice); |
| static void espeak_set_language_and_voice(char *lang, SPDVoiceType voice); |
| static void espeak_set_synthesis_voice(char *); |
| |
| /* > */ |
| /* < Module configuration options*/ |
| |
| MOD_OPTION_1_STR(EspeakPunctuationList) |
| MOD_OPTION_1_INT(EspeakCapitalPitchRise) |
| MOD_OPTION_1_INT(EspeakMinRate) |
| MOD_OPTION_1_INT(EspeakNormalRate) |
| MOD_OPTION_1_INT(EspeakMaxRate) |
| MOD_OPTION_1_INT(EspeakIndexing) |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| MOD_OPTION_1_INT(EspeakMbrola) |
| #endif |
| MOD_OPTION_1_INT(EspeakAudioChunkSize) |
| MOD_OPTION_1_INT(EspeakAudioQueueMaxSize) |
| MOD_OPTION_1_STR(EspeakSoundIconFolder) |
| MOD_OPTION_1_INT(EspeakSoundIconVolume) |
| |
| /* > */ |
| /* < Public functions */ |
| int module_load(void) |
| { |
| INIT_SETTINGS_TABLES(); |
| |
| REGISTER_DEBUG(); |
| |
| /* Options */ |
| #ifdef ESPEAK_NG_INCLUDE |
| MOD_OPTION_1_INT_REG(EspeakMbrola, 0); |
| #endif |
| MOD_OPTION_1_INT_REG(EspeakAudioChunkSize, 2000); |
| MOD_OPTION_1_INT_REG(EspeakAudioQueueMaxSize, 20 * 22050); |
| MOD_OPTION_1_STR_REG(EspeakSoundIconFolder, |
| "/usr/share/sounds/sound-icons/"); |
| MOD_OPTION_1_INT_REG(EspeakSoundIconVolume, 0); |
| |
| MOD_OPTION_1_INT_REG(EspeakMinRate, 80); |
| MOD_OPTION_1_INT_REG(EspeakNormalRate, 170); |
| MOD_OPTION_1_INT_REG(EspeakMaxRate, 449); |
| MOD_OPTION_1_STR_REG(EspeakPunctuationList, "@/+-_"); |
| MOD_OPTION_1_INT_REG(EspeakCapitalPitchRise, 800); |
| MOD_OPTION_1_INT_REG(EspeakIndexing, 1); |
| if (EspeakCapitalPitchRise == 1 || EspeakCapitalPitchRise == 2) { |
| EspeakCapitalPitchRise = 0; |
| } |
| |
| return OK; |
| } |
| |
| int module_init(char **status_info) |
| { |
| int ret; |
| const char *espeak_version; |
| |
| DBG(DBG_MODNAME " Module init()."); |
| |
| module_audio_set_server(); |
| |
| *status_info = NULL; |
| |
| /* Report versions. */ |
| espeak_version = espeak_Info(NULL); |
| DBG(DBG_MODNAME " espeak Output Module version %s, espeak Engine version %s", |
| MODULE_VERSION, espeak_version); |
| |
| /* <Espeak setup */ |
| |
| DBG(DBG_MODNAME " Initializing engine with buffer size %d ms.", |
| EspeakAudioChunkSize); |
| #if ESPEAK_API_REVISION == 1 |
| espeak_sample_rate = |
| espeak_Initialize(AUDIO_OUTPUT_SYNCHRONOUS, EspeakAudioChunkSize, |
| NULL); |
| #else |
| espeak_sample_rate = |
| espeak_Initialize(AUDIO_OUTPUT_SYNCHRONOUS, EspeakAudioChunkSize, |
| NULL, 0); |
| #endif |
| DBG(DBG_MODNAME " Sample rate is %d", espeak_sample_rate); |
| if (espeak_sample_rate == EE_INTERNAL_ERROR) { |
| DBG(DBG_MODNAME " Could not initialize engine."); |
| *status_info = g_strdup("Could not initialize engine. "); |
| return FATAL_ERROR; |
| } |
| |
| DBG(DBG_MODNAME " Registering callbacks."); |
| espeak_SetSynthCallback(synth_callback); |
| espeak_SetUriCallback(uri_callback); |
| |
| DBG("Setting up espeak specific configuration settings."); |
| ret = espeak_set_punctuation_list_from_utf8(EspeakPunctuationList); |
| if (ret != OK) |
| DBG(DBG_MODNAME " Failed to set punctuation list."); |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| #ifdef __linux__ |
| if (EspeakMbrola) { |
| mbrola_voice_inotify = inotify_init1(IN_NONBLOCK|IN_CLOEXEC); |
| if (mbrola_voice_inotify >= 0) { |
| const char *espeak_data; |
| char *path; |
| espeak_Info(&espeak_data); |
| |
| path = g_strdup_printf("%s/mbrola", espeak_data); |
| inotify_add_watch(mbrola_voice_inotify, path, IN_CREATE|IN_DELETE); |
| g_free(path); |
| |
| inotify_add_watch(mbrola_voice_inotify, "/usr/share/mbrola", IN_CREATE|IN_DELETE); |
| inotify_add_watch(mbrola_voice_inotify, "/usr/share/mbrola/voices", IN_CREATE|IN_DELETE); |
| } |
| } |
| #endif |
| #endif |
| |
| espeak_voice_list = espeak_list_synthesis_voices(); |
| if (espeak_voice_list == NULL) { |
| *status_info = g_strdup(DBG_MODNAME " has no voice."); |
| return FATAL_ERROR; |
| } |
| |
| initialized = TRUE; |
| *status_info = g_strdup(DBG_MODNAME " Initialized successfully."); |
| |
| return OK; |
| } |
| |
| SPDVoice **module_list_voices(void) |
| { |
| #ifdef ESPEAK_NG_INCLUDE |
| #ifdef __linux__ |
| if (mbrola_voice_inotify >= 0) { |
| char buf[1024]; |
| struct inotify_event *e = (void*) buf; |
| ssize_t n = read(mbrola_voice_inotify, buf, sizeof(buf)); |
| |
| if (n > 0) { |
| DBG(DBG_MODNAME "Mbrola path %s updated, re-loading voice list", e->name); |
| |
| /* Mbrola voice added or removed */ |
| while (read(mbrola_voice_inotify, buf, sizeof(buf)) > 0) |
| /* Flush all events before we re-read voices */ |
| ; |
| |
| espeak_free_voice_list(); |
| espeak_voice_list = espeak_list_synthesis_voices(); |
| } |
| } |
| #endif |
| #endif |
| return espeak_voice_list; |
| } |
| |
| void module_speak_sync(const gchar * data, size_t bytes, SPDMessageType msgtype) |
| { |
| espeak_ERROR result = EE_INTERNAL_ERROR; |
| int flags = espeakSSML | espeakCHARS_UTF8; |
| gchar *msg = NULL; |
| |
| DBG(DBG_MODNAME " module_speak()."); |
| |
| DBG(DBG_MODNAME " Requested data: |%s| %d %lu", data, msgtype, |
| (unsigned long)bytes); |
| |
| /* Setting speech parameters. */ |
| #ifdef ESPEAK_NG_INCLUDE |
| if (EspeakMbrola) |
| { |
| /* espeak has troubles with setting mbrola voices in SSML mode |
| * https://github.com/espeak-ng/espeak-ng/issues/1011 */ |
| espeak_set_language(msg_settings.voice.language); |
| espeak_set_voice(msg_settings.voice_type); |
| espeak_set_synthesis_voice(msg_settings.voice.name); |
| } |
| else |
| #endif |
| { |
| UPDATE_STRING_PARAMETER(voice.language, espeak_set_language); |
| UPDATE_PARAMETER(voice_type, espeak_set_voice); |
| UPDATE_STRING_PARAMETER(voice.name, espeak_set_synthesis_voice); |
| } |
| |
| UPDATE_PARAMETER(rate, espeak_set_rate); |
| UPDATE_PARAMETER(volume, espeak_set_volume); |
| UPDATE_PARAMETER(pitch, espeak_set_pitch); |
| UPDATE_PARAMETER(pitch_range, espeak_set_pitch_range); |
| UPDATE_PARAMETER(punctuation_mode, espeak_set_punctuation_mode); |
| UPDATE_PARAMETER(cap_let_recogn, espeak_set_cap_let_recogn); |
| |
| began = 0; |
| stop_requested = 0; |
| pause_requested = 0; |
| pause_index_sent = 0; |
| |
| module_speak_ok(); |
| |
| /* |
| UPDATE_PARAMETER(spelling_mode, espeak_set_spelling_mode); |
| */ |
| /* Send data to espeak */ |
| switch (msgtype) { |
| case SPD_MSGTYPE_TEXT: |
| result = espeak_Synth(data, bytes + 1, 0, POS_CHARACTER, 0, |
| flags, NULL, NULL); |
| break; |
| case SPD_MSGTYPE_SOUND_ICON:{ |
| char *msg = |
| g_strdup_printf("<audio src=\"%s%s\">%s</audio>", |
| EspeakSoundIconFolder, data, data); |
| result = |
| espeak_Synth(msg, strlen(msg) + 1, 0, POS_CHARACTER, |
| 0, flags, NULL, NULL); |
| g_free(msg); |
| break; |
| } |
| case SPD_MSGTYPE_CHAR:{ |
| wchar_t wc = 0; |
| if (bytes == 1) { // ASCII |
| wc = (wchar_t) data[0]; |
| } else if (bytes == 5 |
| && (0 == strncmp(data, "space", bytes))) { |
| wc = (wchar_t) 0x20; |
| } else { |
| gsize bytes_out; |
| gchar *tmp = |
| g_convert(data, -1, "wchar_t", "utf-8", |
| NULL, &bytes_out, NULL); |
| if (tmp != NULL && bytes_out == sizeof(wchar_t)) { |
| wchar_t *wc_ptr = (wchar_t *) tmp; |
| wc = wc_ptr[0]; |
| } else { |
| DBG(DBG_MODNAME " Failed to convert utf-8 to wchar_t, or not exactly one utf-8 character given."); |
| } |
| g_free(tmp); |
| } |
| char *msg = |
| g_strdup_printf |
| ("<say-as interpret-as=\"tts:char\">&#%ld;</say-as>", |
| (long)wc); |
| result = |
| espeak_Synth(msg, strlen(msg) + 1, 0, POS_CHARACTER, |
| 0, flags, NULL, NULL); |
| g_free(msg); |
| break; |
| } |
| case SPD_MSGTYPE_KEY:{ |
| const char *key = data; |
| /* Convert unspeakable keys to speakable form, see espeak-ng's ReplaceKeyName */ |
| if (!strcmp(key, " ")) |
| key = "space"; |
| else if (!strcmp(key, "\t")) |
| key = "tab"; |
| else if (!strcmp(key, "_")) |
| key = "underscore"; |
| else if (!strcmp(key, "\"")) |
| key = "double-quote"; |
| char *msg = |
| g_strdup_printf |
| ("<say-as interpret-as=\"tts:key\">%s</say-as>", |
| key); |
| result = |
| espeak_Synth(msg, strlen(msg) + 1, 0, POS_CHARACTER, |
| 0, flags, NULL, NULL); |
| g_free(msg); |
| break; |
| } |
| case SPD_MSGTYPE_SPELL: |
| /* TODO: use a generic engine */ |
| break; |
| } |
| |
| if (result != EE_OK) { |
| DBG(DBG_MODNAME " Synth error %d", result); |
| } |
| |
| if (pause_requested) { |
| DBG(DBG_MODNAME " Synth paused"); |
| module_report_event_pause(); |
| } else if (stop_requested) { |
| DBG(DBG_MODNAME " Synth stopped"); |
| module_report_event_stop(); |
| } else { |
| DBG(DBG_MODNAME " Leaving module_speak() normally."); |
| module_report_event_end(); |
| } |
| |
| g_free(msg); |
| } |
| |
| int module_stop(void) |
| { |
| DBG(DBG_MODNAME " module_stop()."); |
| |
| stop_requested = 1; |
| |
| return OK; |
| } |
| |
| size_t module_pause(void) |
| { |
| DBG(DBG_MODNAME " module_pause()."); |
| |
| pause_requested = 1; |
| |
| return OK; |
| } |
| |
| int module_close(void) |
| { |
| DBG(DBG_MODNAME " close()."); |
| |
| if (!initialized) |
| return 0; |
| |
| DBG(DBG_MODNAME " terminating synthesis."); |
| espeak_Terminate(); |
| |
| espeak_free_voice_list(); |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| #ifdef __linux__ |
| if (mbrola_voice_inotify >= 0) |
| { |
| close(mbrola_voice_inotify); |
| mbrola_voice_inotify = -1; |
| } |
| #endif |
| #endif |
| |
| initialized = FALSE; |
| |
| return 0; |
| } |
| |
| /* > */ |
| /* < Internal functions */ |
| static void espeak_set_rate(signed int rate) |
| { |
| assert(rate >= -100 && rate <= +100); |
| int speed; |
| int normal_rate = EspeakNormalRate, max_rate = EspeakMaxRate, min_rate = |
| EspeakMinRate; |
| |
| if (rate < 0) |
| speed = normal_rate + (normal_rate - min_rate) * rate / 100; |
| else |
| speed = normal_rate + (max_rate - normal_rate) * rate / 100; |
| |
| espeak_ERROR ret = espeak_SetParameter(espeakRATE, speed, 0); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Error setting rate %i.", speed); |
| } else { |
| DBG(DBG_MODNAME " Rate set to %i.", speed); |
| } |
| } |
| |
| static void espeak_set_volume(signed int volume) |
| { |
| assert(volume >= -100 && volume <= +100); |
| int vol; |
| vol = volume + 100; |
| espeak_ERROR ret = espeak_SetParameter(espeakVOLUME, vol, 0); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Error setting volume %i.", vol); |
| } else { |
| DBG(DBG_MODNAME " Volume set to %i.", vol); |
| } |
| } |
| |
| static void espeak_set_pitch(signed int pitch) |
| { |
| assert(pitch >= -100 && pitch <= +100); |
| int pitchBaseline; |
| /* Possible range 0 to 100. */ |
| if (pitch < 0) { |
| pitchBaseline = |
| ((float)(pitch + 100) * espeak_voice_pitch_baseline) / |
| (float)100; |
| } else { |
| pitchBaseline = |
| (((float)pitch * (100 - espeak_voice_pitch_baseline)) |
| / (float)100) + espeak_voice_pitch_baseline; |
| } |
| assert(pitchBaseline >= 0 && pitchBaseline <= 100); |
| espeak_ERROR ret = espeak_SetParameter(espeakPITCH, pitchBaseline, 0); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Error setting pitch %i.", pitchBaseline); |
| } else { |
| DBG(DBG_MODNAME " Pitch set to %i.", pitchBaseline); |
| } |
| } |
| |
| static void espeak_set_pitch_range(signed int pitch_range) |
| { |
| assert(pitch_range >= -100 && pitch_range <= +100); |
| int pitchRangeBaseline; |
| /* Possible range 0 to 100. */ |
| if (pitch_range < 0) { |
| pitchRangeBaseline = |
| ((float)(pitch_range + 100) * |
| espeak_voice_pitch_range_baseline) / (float)100; |
| } else { |
| pitchRangeBaseline = |
| (((float)pitch_range * |
| (100 - espeak_voice_pitch_range_baseline)) |
| / (float)100) + espeak_voice_pitch_range_baseline; |
| } |
| assert(pitchRangeBaseline >= 0 && pitchRangeBaseline <= 100); |
| espeak_ERROR ret = |
| espeak_SetParameter(espeakRANGE, pitchRangeBaseline, 0); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Error setting pitch range %i.", |
| pitchRangeBaseline); |
| } else { |
| DBG(DBG_MODNAME " Pitch range set to %i.", pitchRangeBaseline); |
| } |
| } |
| |
| static void espeak_set_punctuation_mode(SPDPunctuation punct_mode) |
| { |
| espeak_PUNCT_TYPE espeak_punct_mode = espeakPUNCT_SOME; |
| switch (punct_mode) { |
| case SPD_PUNCT_ALL: |
| espeak_punct_mode = espeakPUNCT_ALL; |
| break; |
| case SPD_PUNCT_MOST: |
| /* XXX: Approximation */ |
| espeak_punct_mode = espeakPUNCT_SOME; |
| break; |
| case SPD_PUNCT_SOME: |
| espeak_punct_mode = espeakPUNCT_SOME; |
| break; |
| case SPD_PUNCT_NONE: |
| espeak_punct_mode = espeakPUNCT_NONE; |
| break; |
| } |
| |
| espeak_ERROR ret = |
| espeak_SetParameter(espeakPUNCTUATION, espeak_punct_mode, 0); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Failed to set punctuation mode to %d.", espeak_punct_mode); |
| } else { |
| DBG("Set punctuation mode."); |
| } |
| } |
| |
| static void espeak_set_cap_let_recogn(SPDCapitalLetters cap_mode) |
| { |
| int espeak_cap_mode = 0; |
| switch (cap_mode) { |
| case SPD_CAP_NONE: |
| espeak_cap_mode = EspeakCapitalPitchRise; |
| break; |
| case SPD_CAP_SPELL: |
| espeak_cap_mode = 2; |
| break; |
| case SPD_CAP_ICON: |
| espeak_cap_mode = 1; |
| break; |
| } |
| |
| espeak_ERROR ret = |
| espeak_SetParameter(espeakCAPITALS, espeak_cap_mode, 1); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Failed to set capitals mode to %d.", espeak_cap_mode); |
| } else { |
| DBG("Set capitals mode."); |
| } |
| } |
| |
| /* Given a language code and SD voice code, sets the espeak voice. */ |
| static void espeak_set_language_and_voice(char *lang, SPDVoiceType voice_code) |
| { |
| DBG(DBG_MODNAME " set_language_and_voice %s %d", lang, voice_code); |
| espeak_ERROR ret; |
| |
| espeak_VOICE voice_select; |
| memset(&voice_select, 0, sizeof(voice_select)); |
| voice_select.languages = lang; |
| |
| switch (voice_code) { |
| case SPD_MALE1: |
| voice_select.gender = 1; |
| break; |
| case SPD_MALE2: |
| voice_select.gender = 1; |
| voice_select.variant = 1; |
| break; |
| case SPD_MALE3: |
| voice_select.gender = 1; |
| voice_select.variant = 2; |
| break; |
| case SPD_FEMALE1: |
| voice_select.gender = 2; |
| break; |
| case SPD_FEMALE2: |
| voice_select.gender = 2; |
| voice_select.variant = 1; |
| break; |
| case SPD_FEMALE3: |
| voice_select.gender = 2; |
| voice_select.variant = 2; |
| break; |
| case SPD_CHILD_MALE: |
| voice_select.gender = 1; |
| voice_select.age = 10; |
| break; |
| case SPD_CHILD_FEMALE: |
| voice_select.gender = 2; |
| voice_select.age = 10; |
| break; |
| default: |
| break; |
| } |
| |
| ret = espeak_SetVoiceByProperties(&voice_select); |
| |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Error selecting language %s", lang); |
| } else { |
| DBG(DBG_MODNAME " Successfully set voice to \"%s\"", lang); |
| } |
| } |
| |
| static void espeak_set_voice(SPDVoiceType voice) |
| { |
| assert(msg_settings.voice.language); |
| espeak_set_language_and_voice(msg_settings.voice.language, voice); |
| } |
| |
| static void espeak_set_language(char *lang) |
| { |
| espeak_set_language_and_voice(lang, msg_settings.voice_type); |
| } |
| |
| static void espeak_set_synthesis_voice(char *synthesis_voice) |
| { |
| if (synthesis_voice != NULL) { |
| #ifdef ESPEAK_NG_INCLUDE |
| gchar *voice_name = NULL; |
| gchar *variant_name = NULL; |
| gchar **voice_split = NULL; |
| gchar **identifier = NULL; |
| gchar *variant_file = NULL; |
| gchar *voice = NULL; |
| int i = 0; |
| |
| /* Espeak-ng can accept the full voice name as the voice, but will |
| * only accept the file name of the variant to use, which can be |
| * found in the identifier member of the variant list, which |
| * itself is of type espeak_VOICE |
| */ |
| if (g_strstr_len(synthesis_voice, -1, "+") != NULL) { |
| voice_split = g_strsplit(synthesis_voice, "+", 2); |
| voice_name = voice_split[0]; |
| variant_name = voice_split[1]; |
| g_free(voice_split); |
| |
| for (i = 0; espeak_variants_array[i].name != NULL; i++) { |
| identifier = g_strsplit(espeak_variants_array[i].identifier, "/", 2); |
| |
| if (g_strcmp0(espeak_variants_array[i].name, variant_name) == 0) { |
| if (identifier[1] != NULL) |
| variant_file = g_strdup(identifier[1]); |
| } else if (g_strcmp0(identifier[1], variant_name) == 0) { |
| variant_file = g_strdup(variant_name); |
| } |
| |
| g_strfreev(identifier); |
| identifier = NULL; |
| } |
| |
| if (variant_file != NULL) { |
| voice = synthesis_voice = g_strdup_printf("%s+%s", voice_name, variant_file); |
| g_free(variant_file); |
| } else { |
| DBG(DBG_MODNAME " Cannot find the variant file name for the given variant."); |
| } |
| |
| g_free(voice_name); |
| g_free(variant_name); |
| } |
| #endif |
| |
| espeak_ERROR ret = espeak_SetVoiceByName(synthesis_voice); |
| if (ret != EE_OK) { |
| DBG(DBG_MODNAME " Failed to set synthesis voice to %s.", |
| synthesis_voice); |
| } else { |
| DBG(DBG_MODNAME " Successfully set synthesis voice to %s.", |
| synthesis_voice); |
| } |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| g_free(voice); |
| #endif |
| } |
| } |
| |
| /* Callbacks */ |
| |
| static gboolean espeak_send_audio_upto(short *wav, int *sent, int upto) |
| { |
| assert(*sent >= 0); |
| assert(upto >= 0); |
| int numsamples = upto - (*sent); |
| if (wav == NULL || numsamples == 0) { |
| return TRUE; |
| } |
| AudioTrack track = { |
| .bits = 16, |
| .num_channels = 1, |
| .sample_rate = espeak_sample_rate, |
| .num_samples = numsamples, |
| .samples = wav + (*sent), |
| }; |
| DBG(DBG_MODNAME " pushing %d samples", numsamples); |
| module_tts_output_server(&track, SPD_AUDIO_LE); |
| *sent = upto; |
| return 0; |
| } |
| |
| static int synth_callback(short *wav, int numsamples, espeak_EVENT * events) |
| { |
| /* Number of samples sent in current message. */ |
| static int numsamples_sent_msg = 0; |
| /* Number of samples already sent during this call to the callback. */ |
| int numsamples_sent = 0; |
| int first = 1, nextfirst; |
| |
| /* Process server events in case we were told to stop in between */ |
| module_process(STDIN_FILENO, 0); |
| |
| if (stop_requested) |
| return 1; |
| |
| if (pause_requested && pause_index_sent) |
| return 1; |
| |
| if (!began) { |
| began = 1; |
| module_report_event_begin(); |
| numsamples_sent_msg = 0; |
| } |
| |
| /* Process events and audio data */ |
| while (events->type != espeakEVENT_LIST_TERMINATED) { |
| nextfirst = 0; |
| /* Enqueue audio upto event */ |
| switch (events->type) { |
| case espeakEVENT_MARK: |
| if (!EspeakIndexing) |
| break; |
| case espeakEVENT_PLAY:{ |
| /* Convert ms position to samples */ |
| gint64 pos_msg = events->audio_position; |
| pos_msg = pos_msg * espeak_sample_rate / 1000; |
| /* Convert position in message to position in current chunk */ |
| int upto = |
| (int)CLAMP(pos_msg - numsamples_sent_msg, |
| 0, numsamples); /* This is just for safety */ |
| espeak_send_audio_upto(wav, &numsamples_sent, |
| upto); |
| break; |
| } |
| default: |
| break; |
| } |
| if (stop_requested) |
| return 1; |
| /* Process actual event */ |
| switch (events->type) { |
| case espeakEVENT_SENTENCE: |
| case espeakEVENT_WORD: |
| case espeakEVENT_PHONEME: |
| case espeakEVENT_END: |
| // Ignore |
| break; |
| |
| case espeakEVENT_MARK: |
| if (EspeakIndexing) { |
| DBG(DBG_MODNAME " Reporting mark %s", events->id.name); |
| module_report_index_mark(events->id.name); |
| if (pause_requested && |
| !strncmp(events->id.name, |
| INDEX_MARK_BODY, |
| INDEX_MARK_BODY_LEN)) { |
| pause_index_sent = 1; |
| return 1; |
| } |
| } |
| break; |
| case espeakEVENT_PLAY: |
| module_report_icon(events->id.name); |
| break; |
| case espeakEVENT_MSG_TERMINATED: |
| // This event never has any audio in the same callback |
| DBG(DBG_MODNAME " Synth terminated"); |
| break; |
| case espeakEVENT_SAMPLERATE: |
| DBG(DBG_MODNAME " Got sample rate %d", events->id.number); |
| if (first) { |
| /* espeak-ng currently seems to produce odd |
| * sample rate changes, so ignore them but the |
| * initial event for now. |
| * See https://github.com/espeak-ng/espeak-ng/issues/2028 */ |
| espeak_sample_rate = events->id.number; |
| /* Keep looking at sample rate changes until we get something else */ |
| nextfirst = 1; |
| } |
| break; |
| default: |
| DBG(DBG_MODNAME " Got unsupported event %d\n", events->type); |
| break; |
| } |
| if (stop_requested) |
| return 1; |
| events++; |
| first = nextfirst; |
| } |
| espeak_send_audio_upto(wav, &numsamples_sent, numsamples); |
| numsamples_sent_msg += numsamples; |
| if (stop_requested) |
| return 1; |
| return 0; |
| } |
| |
| static int uri_callback(int type, const char *uri, const char *base) |
| { |
| int result = 1; |
| if (type == 1) { |
| /* Audio icon */ |
| if (g_file_test(uri, G_FILE_TEST_EXISTS)) { |
| result = 0; |
| } |
| } |
| return result; |
| } |
| |
| static SPDVoice **espeak_list_synthesis_voices() |
| { |
| SPDVoice **result = NULL; |
| SPDVoice *voice = NULL; |
| SPDVoice *vo = NULL; |
| const espeak_VOICE **espeak_voices = NULL; |
| const espeak_VOICE **espeak_variants = NULL; |
| const espeak_VOICE **espeak_mbrola = NULL; |
| espeak_VOICE voice_spec; |
| const espeak_VOICE *v = NULL; |
| GQueue *voice_list = NULL; |
| GQueue *variant_list = NULL; |
| GList *voice_list_iter = NULL; |
| GList *variant_list_iter = NULL; |
| const gchar *first_lang = NULL; |
| gchar *dash; |
| gchar *vname = NULL; |
| int numvoices = 0; |
| int numvariants = 0; |
| int nummbrola = 0, totnummbrola = 0; |
| int totalvoices; |
| int i = 0, j; |
| |
| espeak_voices = espeak_ListVoices(NULL); |
| voice_list = g_queue_new(); |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| if (!EspeakMbrola) |
| #endif |
| { |
| for (i = 0; espeak_voices[i] != NULL; i++) { |
| v = espeak_voices[i]; |
| if (!g_str_has_prefix(v->identifier, "mb/")) { |
| /* Not an mbrola voice */ |
| voice = g_new0(SPDVoice, 1); |
| |
| voice->name = g_strdup(v->name); |
| |
| first_lang = v->languages + 1; |
| voice->language = g_strdup(first_lang); |
| for (dash = strchr(voice->language, '-'); |
| dash && *dash; |
| dash++) { |
| *dash = toupper(*dash); |
| } |
| voice->variant = NULL; |
| |
| g_queue_push_tail(voice_list, voice); |
| } |
| |
| } |
| } |
| |
| numvoices = g_queue_get_length(voice_list); |
| DBG(DBG_MODNAME " %d voices total.", numvoices); |
| #ifdef ESPEAK_NG_INCLUDE |
| if (!EspeakMbrola) |
| #endif |
| { |
| if (numvoices == 0) { |
| return NULL; |
| } |
| } |
| |
| memset(&voice_spec, 0, sizeof(voice_spec)); |
| voice_spec.languages = "variant"; |
| espeak_variants = espeak_ListVoices(&voice_spec); |
| |
| variant_list = g_queue_new(); |
| |
| for (i = 0; espeak_variants[i] != NULL; i++) { |
| v = espeak_variants[i]; |
| |
| vname = g_strdup(v->name); |
| g_queue_push_tail(variant_list, vname); |
| } |
| |
| numvariants = g_queue_get_length(variant_list); |
| DBG(DBG_MODNAME " %d variants total.", numvariants); |
| |
| #ifdef ESPEAK_NG_INCLUDE |
| espeak_variants_array = malloc((numvariants+1) * sizeof(*espeak_variants_array)); |
| for (i = 0; i < numvariants; i++) { |
| v = espeak_variants[i]; |
| espeak_variants_array[i].name = g_strdup(v->name); |
| espeak_variants_array[i].identifier = g_strdup(v->identifier); |
| } |
| espeak_variants_array[numvariants].name = NULL; |
| |
| if (EspeakMbrola) |
| { |
| voice_spec.languages = "mbrola"; |
| espeak_mbrola = espeak_ListVoices(&voice_spec); |
| const char *espeak_data; |
| |
| espeak_Info(&espeak_data); |
| |
| for (j = 0; espeak_mbrola[j] != NULL; j++) |
| { |
| const char *identifier = espeak_mbrola[j]->identifier; |
| char *voicename, *dash, *path; |
| |
| totnummbrola++; |
| |
| /* We try to load the voice to check whether it works, but |
| * espeak-ng is not currently reporting load failures, see |
| * https://github.com/espeak-ng/espeak-ng/pull/1022 */ |
| |
| if (!strncmp(identifier, "mb/mb-", 6)) { |
| voicename = g_strdup(identifier + 6); |
| dash = strchr(voicename, '-'); |
| if (dash) |
| /* Ignore "-en" language specification */ |
| *dash = 0; |
| |
| path = g_strdup_printf("%s/mbrola/%s", espeak_data, voicename); |
| if (access(path, O_RDONLY) != 0) { |
| g_free(path); |
| path = g_strdup_printf("/usr/share/mbrola/%s", voicename); |
| if (access(path, O_RDONLY) != 0) { |
| g_free(path); |
| path = g_strdup_printf("/usr/share/mbrola/%s/%s", voicename, voicename); |
| if (access(path, O_RDONLY) != 0) { |
| g_free(path); |
| path = g_strdup_printf("/usr/share/mbrola/voices/%s", voicename); |
| if (access(path, O_RDONLY) != 0) { |
| g_free(path); |
| espeak_mbrola[j] = NULL; |
| continue; |
| } |
| } |
| } |
| } |
| g_free(path); |
| } |
| |
| espeak_ERROR ret = espeak_SetVoiceByName(espeak_mbrola[j]->name); |
| if (ret != EE_OK) { |
| espeak_mbrola[j] = NULL; |
| continue; |
| } |
| |
| nummbrola++; |
| } |
| } |
| |
| DBG(DBG_MODNAME " %d mbrola total.", nummbrola); |
| #endif |
| |
| totalvoices = (numvoices * (numvariants + 1)) + nummbrola; |
| |
| if (totalvoices == 0) { |
| if (voice_list != NULL) |
| g_queue_free(voice_list); |
| if (variant_list != NULL) |
| g_queue_free_full(variant_list, (GDestroyNotify)g_free); |
| return NULL; |
| } |
| |
| result = g_new0(SPDVoice *, totalvoices + 1); |
| voice_list_iter = g_queue_peek_head_link(voice_list); |
| |
| for (i = 0; i < numvoices * (numvariants + 1); i++) { |
| result[i] = voice_list_iter->data; |
| |
| if (!g_queue_is_empty(variant_list)) { |
| vo = voice_list_iter->data; |
| variant_list_iter = g_queue_peek_head_link(variant_list); |
| |
| while (variant_list_iter != NULL && variant_list_iter->data != NULL) { |
| voice = g_new0(SPDVoice, 1); |
| |
| voice->name = g_strdup_printf("%s+%s", vo->name, |
| (char *)variant_list_iter->data); |
| voice->language = g_strdup(vo->language); |
| voice->variant = g_strdup((char *)variant_list_iter->data); |
| |
| result[++i] = voice; |
| variant_list_iter = variant_list_iter->next; |
| } |
| } |
| |
| voice_list_iter = voice_list_iter->next; |
| } |
| |
| for (j = 0; j < totnummbrola; j++) { |
| if (!espeak_mbrola[j]) |
| continue; |
| |
| voice = g_new0(SPDVoice, 1); |
| voice->name = g_strdup_printf("%s", espeak_mbrola[j]->name); |
| voice->language = g_strdup_printf("%s", espeak_mbrola[j]->languages + 1); |
| for (dash = strchr(voice->language, '-'); |
| dash && *dash; |
| dash++) { |
| *dash = toupper(*dash); |
| } |
| voice->variant = NULL; |
| result[i++] = voice; |
| } |
| |
| assert(i == totalvoices); |
| |
| if (voice_list != NULL) |
| g_queue_free(voice_list); |
| if (variant_list != NULL) |
| g_queue_free_full(variant_list, (GDestroyNotify)g_free); |
| |
| result[i] = NULL; |
| DBG(DBG_MODNAME " %d usable voices.", totalvoices); |
| |
| return result; |
| } |
| |
| static void espeak_free_voice_list() |
| { |
| #ifdef ESPEAK_NG_INCLUDE |
| free(espeak_variants_array); |
| #endif |
| if (espeak_voice_list != NULL) { |
| int i; |
| for (i = 0; espeak_voice_list[i] != NULL; i++) { |
| g_free(espeak_voice_list[i]->name); |
| g_free(espeak_voice_list[i]->language); |
| g_free(espeak_voice_list[i]->variant); |
| g_free(espeak_voice_list[i]); |
| } |
| g_free(espeak_voice_list); |
| espeak_voice_list = NULL; |
| } |
| } |
| |
| static TEspeakSuccess espeak_set_punctuation_list_from_utf8(const gchar * punct) |
| { |
| TEspeakSuccess result = ERROR; |
| wchar_t *wc_punct = |
| (wchar_t *) g_convert(punct, -1, "wchar_t", "utf-8", NULL, NULL, |
| NULL); |
| if (wc_punct != NULL) { |
| espeak_ERROR ret = espeak_SetPunctuationList(wc_punct); |
| if (ret == EE_OK) |
| result = OK; |
| g_free(wc_punct); |
| } |
| return result; |
| } |
| |
| /* > */ |
| |
| /* local variables: */ |
| /* folded-file: t */ |
| /* c-basic-offset: 4 */ |
| /* end: */ |