accessibility: add selectable speech-dispatcher narrator engine (Linux)

Add speech-dispatcher as an optional Text-to-Speech backend for the
Linux/Unix narrator, selectable via a new 'Text-to-Speech Engine'
accessibility setting. espeak remains the default, so existing setups
are completely unaffected.

The narrator was previously hard-wired to fork()/execvp("espeak").
speech-dispatcher is invoked the same way, via its spd-say CLI, rather
than by linking libspeechd. This keeps it a pure runtime option:

  - No new build-time or link-time dependency, hence no configure
    plumbing and no buildbot/CI impact. If spd-say is not installed,
    execvp fails exactly as a missing espeak does today and the
    existing RARCH_WARN path handles it.
  - The espeak argv construction (the -v voice / -s speed flags and the
    language-code mapping) is left intact; the speech-dispatcher path
    builds its own argv (spd-say -l <lang> -r <rate> -w), since the two
    engines use different CLI conventions. The 1..10 speed ladder is
    mapped to spd-say's -100..100 rate range. -w keeps the child alive
    so the existing is_narrator_running_unix()/SIGTERM interrupt logic
    works identically for both engines.
  - DEFAULT_ACCESSIBILITY_NARRATOR_ENGINE is espeak (enum index 0), so
    a default or upgraded config selects the current behaviour with no
    change in output.

The menu entry is gated to non-Android Linux, matching where the
speech-dispatcher path is meaningful. config_get_ptr() is NULL-checked
in the speak path and falls back to espeak if settings are unavailable.

The engine enum lives in configuration.h so both the frontend driver
and the menu can reference it without pulling in config.def.h.

Note: speech-dispatcher requires a running daemon with a configured
output module; that is a user-environment concern and is precisely why
espeak is retained as the default rather than replaced.
This commit is contained in:
libretroadmin authored and libretroadmin committed 2026-06-25 08:48:31 +02:00
1 parent 568dd77fc0
commit f5498b2709
7 files changed
+105 -3

No files matched your search

+4
View File
@@ -188,6 +188,10 @@
#define DEFAULT_ACCESSIBILITY_NARRATOR_SPEECH_SPEED 5
/* espeak is the default narrator engine so existing setups are
* unaffected. The enum lives in configuration.h. */
#define DEFAULT_ACCESSIBILITY_NARRATOR_ENGINE ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK
#define DEFAULT_DRIVER_SWITCH_ENABLE true
#define DEFAULT_USER_LANGUAGE 0
+1
View File
@@ -2710,6 +2710,7 @@ static struct config_uint_setting *populate_settings_uint(
SETTING_UINT("cheevos_visibility_summary", &settings->uints.cheevos_visibility_summary, true, DEFAULT_CHEEVOS_VISIBILITY_SUMMARY, false);
#endif
SETTING_UINT("accessibility_narrator_speech_speed", &settings->uints.accessibility_narrator_speech_speed, true, DEFAULT_ACCESSIBILITY_NARRATOR_SPEECH_SPEED, false);
SETTING_UINT("accessibility_narrator_engine", &settings->uints.accessibility_narrator_engine, true, DEFAULT_ACCESSIBILITY_NARRATOR_ENGINE, false);
SETTING_UINT("ai_service_mode", &settings->uints.ai_service_mode, true, DEFAULT_AI_SERVICE_MODE, false);
SETTING_UINT("ai_service_target_lang", &settings->uints.ai_service_target_lang, true, 0, false);
SETTING_UINT("ai_service_source_lang", &settings->uints.ai_service_source_lang, true, 0, false);
+10
View File
@@ -102,6 +102,15 @@ enum settings_glob_flags
SETTINGS_FLG_SKIP_WINDOW_POSITIONS = (1 << 1)
};
/* Narrator (TTS) engine for the Linux/Unix accessibility path.
* espeak is index 0 / default so existing setups are unaffected. */
enum accessibility_narrator_engine_enum
{
ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK = 0,
ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER,
ACCESSIBILITY_NARRATOR_ENGINE_LAST
};
typedef struct settings
{
struct
@@ -293,6 +302,7 @@ typedef struct settings
/* Accessibility */
unsigned accessibility_narrator_speech_speed;
unsigned accessibility_narrator_engine;
unsigned menu_timedate_style;
unsigned menu_timedate_date_separator;
+33 -2
View File
@@ -3351,10 +3351,17 @@ static bool accessibility_speak_unix(int speed,
const char* speak_text, int priority)
{
int pid;
settings_t *settings = config_get_ptr();
unsigned engine = settings
? settings->uints.accessibility_narrator_engine
: (unsigned)ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK;
const char* language = accessibility_unix_language_code(get_user_language_iso639_1(true));
char* voice_out = (char*)malloc(3 + strlen(language));
char* speed_out = (char*)malloc(3 + 3);
const char* speeds[10] = {"80", "100", "125", "150", "170", "210", "260", "310", "380", "450"};
/* speech-dispatcher (spd-say) takes a rate in the range -100..100,
* mapped here from RetroArch's 1..10 speed ladder. */
const char* spd_rates[10] = {"-90", "-70", "-50", "-30", "-10", "10", "30", "50", "70", "90"};
/* NULL-check both mallocs: voice_out[0]='-' and speed_out[0]='-'
* below NULL-deref on OOM. The 'end:' label does NULL-tolerant
@@ -3390,7 +3397,7 @@ static bool accessibility_speak_unix(int speed,
if (speak_pid > 0)
{
/* Kill the running espeak */
/* Kill the running narrator */
kill(speak_pid, SIGTERM);
speak_pid = 0;
}
@@ -3399,6 +3406,30 @@ static bool accessibility_speak_unix(int speed,
switch (pid)
{
case 0:
if (engine == ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER)
{
/* child process: speech-dispatcher via spd-say.
* -l <lang> language
* -r <rate> rate (-100..100)
* -w wait until finished, so this child stays
* alive for is_narrator_running_unix()/SIGTERM
* to track and interrupt, matching the espeak
* long-lived-child behaviour. */
char* cmd[] = { (char*) "spd-say",
(char*) "-l", NULL,
(char*) "-r", NULL,
(char*) "-w",
NULL, NULL };
cmd[2] = (char*)language;
cmd[4] = (char*)spd_rates[speed-1];
cmd[6] = (char*)speak_text;
execvp("spd-say", cmd);
RARCH_WARN("Could not execute spd-say.\n");
/* Prevent interfere with the parent process */
_exit(EXIT_FAILURE);
}
else
{
/* child process: replace process with the espeak command */
char* cmd[] = { (char*) "espeak", NULL, NULL, NULL, NULL };
@@ -3412,7 +3443,7 @@ static bool accessibility_speak_unix(int speed,
_exit(EXIT_FAILURE);
}
case -1:
RARCH_ERR("Could not fork for espeak.\n");
RARCH_ERR("Could not fork for narrator.\n");
default:
{
/* parent process */
+16
View File
@@ -7280,6 +7280,22 @@ MSG_HASH(
MENU_ENUM_SUBLABEL_ACCESSIBILITY_NARRATOR_SPEECH_SPEED,
"The speed for the Text-to-Speech voice."
)
MSG_HASH(
MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_NARRATOR_ENGINE,
"Text-to-Speech Engine"
)
MSG_HASH(
MENU_ENUM_SUBLABEL_ACCESSIBILITY_NARRATOR_ENGINE,
"The Text-to-Speech backend used for the narrator."
)
MSG_HASH(
MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK,
"eSpeak"
)
MSG_HASH(
MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER,
"Speech Dispatcher"
)
/* Settings > Power Management */
+38 -1
View File
@@ -3315,9 +3315,27 @@ static size_t setting_get_string_representation_uint_ai_service_mode(
return 0;
}
static size_t setting_get_string_representation_uint_ai_service_lang(
static size_t setting_get_string_representation_uint_accessibility_narrator_engine(
rarch_setting_t *setting, char *s, size_t len)
{
enum msg_hash_enums enum_idx = MSG_UNKNOWN;
if (!setting)
return 0;
switch (*setting->value.target.unsigned_integer)
{
case ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK:
enum_idx = MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK;
break;
case ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER:
enum_idx = MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER;
break;
default:
break;
}
if (enum_idx != 0)
return strlcpy(s, msg_hash_to_str(enum_idx), len);
return 0;
}
enum msg_hash_enums enum_idx = MSG_UNKNOWN;
if (!setting)
return 0;
@@ -21562,6 +21580,25 @@ static bool setting_append_list(
general_read_handler);
menu_settings_list_current_add_range(list, list_info, 1, 10, 1, true, true);
#if defined(__linux__) && !defined(ANDROID)
CONFIG_UINT(
list, list_info,
&settings->uints.accessibility_narrator_engine,
MENU_ENUM_LABEL_ACCESSIBILITY_NARRATOR_ENGINE,
MENU_ENUM_LABEL_VALUE_ACCESSIBILITY_NARRATOR_ENGINE,
DEFAULT_ACCESSIBILITY_NARRATOR_ENGINE,
&group_info,
&subgroup_info,
parent_group,
general_write_handler,
general_read_handler);
(*list)[list_info->index - 1].get_string_representation =
&setting_get_string_representation_uint_accessibility_narrator_engine;
(*list)[list_info->index - 1].action_ok = &setting_action_ok_uint;
menu_settings_list_current_add_range(list, list_info, 0,
ACCESSIBILITY_NARRATOR_ENGINE_LAST - 1, 1, true, true);
#endif
END_SUB_GROUP(list, list_info, parent_group);
END_GROUP(list, list_info, parent_group);
break;
+3
View File
@@ -2909,6 +2909,9 @@ enum msg_hash_enums
MENU_LABEL(ACCESSIBILITY_SETTINGS),
MENU_LABEL(ACCESSIBILITY_ENABLED),
MENU_LABEL(ACCESSIBILITY_NARRATOR_SPEECH_SPEED),
MENU_LABEL(ACCESSIBILITY_NARRATOR_ENGINE),
MENU_LABEL(ACCESSIBILITY_NARRATOR_ENGINE_ESPEAK),
MENU_LABEL(ACCESSIBILITY_NARRATOR_ENGINE_SPEECH_DISPATCHER),
MENU_LABEL(AI_SERVICE_SETTINGS),
MENU_LABEL(AI_SERVICE_MODE),
MENU_LABEL(AI_SERVICE_BACKEND),