blob: a633597d767dadf9f4be5f0790f7b3d8badd7ddc [file] [log] [blame]
/*
* 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: */