Files
Shivansps 20b55eb58c TTS Speech rework (#7357)
* add imgui speech options

* adapt existing windows sapi speech implementation

* adapt existing mac speech integration

* add speech linux stubs

* add speech support in linux

* Add array checks

* Use dlopen for speech-dispatcher

* corrrect lib name

* missing includes and static cast

* do not change mac file type

* fix clang tidy warnings 1

* set tts rate

* set localization ids

* fix clang tidy warnings 2

* correct symbol name

* Remove voice cache and fix win enumerate_voices overriding voice selection

* fix mac rate

Done by notimaginative

* requested changes

* re-add voice cache for linux

* Open connection for linux get flags

* fix missing }

* change voice option combobox to std::pair

* delete duplicated voice id sanitizer on windows set voice

* Use pairs for speech_enumerate_voices() and adapt linux speech

* use reference

* actually free vector memory

* Clear voice cache when ingame options closes

* Use extern to call fsspeech_options_cleanup()

* update localization ids

* Fixing some linux and window speech oversights

* Apply volume and rate after a voice change, correct voice, volume and rate apply order
2026-05-14 08:07:11 -04:00

283 lines
6.9 KiB
C++

#ifdef FS2_SPEECH
#include <dlfcn.h>
#include "globalincs/pstypes.h"
#include "utils/unicode.h"
#include "speech.h"
// Adapted from libspeechd.h / speechd_types.h
// https://github.com/brailcom/speechd/tree/master/src/api/c
typedef struct SPDConnection SPDConnection;
typedef struct {
char *name;
char *language;
char *variant;
} SPDVoice;
typedef enum {
SPD_MODE_SINGLE = 0,
SPD_MODE_THREADED = 1
} SPDConnectionMode;
typedef enum {
SPD_IMPORTANT = 1,
SPD_MESSAGE = 2,
SPD_TEXT = 3,
SPD_NOTIFICATION = 4,
SPD_PROGRESS = 5
} SPDPriority;
static void* lib_handle = nullptr;
typedef SPDConnection* (*pfn_spd_open)(const char*, const char*, const char*, SPDConnectionMode);
typedef void (*pfn_spd_close)(SPDConnection*);
typedef int (*pfn_spd_say)(SPDConnection*, SPDPriority, const char*);
typedef int (*pfn_spd_pause)(SPDConnection*);
typedef int (*pfn_spd_resume)(SPDConnection*);
typedef int (*pfn_spd_stop)(SPDConnection*);
typedef int (*pfn_spd_set_volume)(SPDConnection*, signed int);
typedef int (*pfn_spd_set_synthesis_voice)(SPDConnection*, const char*);
typedef int (*pfn_spd_set_voice_rate)(SPDConnection*, signed int);
typedef SPDVoice** (*pfn_spd_list_synthesis_voices)(SPDConnection*);
typedef void (*pfn_free_spd_voices)(SPDVoice**);
static pfn_spd_open p_spd_open = nullptr;
static pfn_spd_close p_spd_close = nullptr;
static pfn_spd_say p_spd_say = nullptr;
static pfn_spd_pause p_spd_pause = nullptr;
static pfn_spd_resume p_spd_resume = nullptr;
static pfn_spd_stop p_spd_stop = nullptr;
static pfn_spd_set_volume p_spd_set_volume = nullptr;
static pfn_spd_set_synthesis_voice p_spd_set_synthesis_voice = nullptr;
static pfn_spd_list_synthesis_voices p_spd_list_synthesis_voices = nullptr;
static pfn_spd_set_voice_rate p_spd_set_voice_rate = nullptr;
static pfn_free_spd_voices p_free_spd_voices = nullptr;
// Load speech-dispatcher with dlopen and load symbols
static bool ensure_speechd_lib()
{
if (lib_handle) return true;
lib_handle = dlopen("libspeechd.so.2", RTLD_LAZY | RTLD_LOCAL);
if (!lib_handle) {
lib_handle = dlopen("libspeechd.so", RTLD_LAZY | RTLD_LOCAL);
}
if (!lib_handle) {
mprintf(("Speech: Unable to load libspeechd.so: %s\n", dlerror()));
return false;
}
// used symbols
p_spd_open = (pfn_spd_open) dlsym(lib_handle, "spd_open");
p_spd_close = (pfn_spd_close) dlsym(lib_handle, "spd_close");
p_spd_say = (pfn_spd_say) dlsym(lib_handle, "spd_say");
p_spd_pause = (pfn_spd_pause) dlsym(lib_handle, "spd_pause");
p_spd_resume = (pfn_spd_resume) dlsym(lib_handle, "spd_resume");
p_spd_stop = (pfn_spd_stop) dlsym(lib_handle, "spd_stop");
p_spd_set_volume = (pfn_spd_set_volume) dlsym(lib_handle, "spd_set_volume");
p_spd_set_synthesis_voice = (pfn_spd_set_synthesis_voice) dlsym(lib_handle, "spd_set_synthesis_voice");
p_spd_list_synthesis_voices = (pfn_spd_list_synthesis_voices) dlsym(lib_handle, "spd_list_synthesis_voices");
p_spd_set_voice_rate = (pfn_spd_set_voice_rate) dlsym(lib_handle, "spd_set_voice_rate");
p_free_spd_voices = (pfn_free_spd_voices) dlsym(lib_handle, "free_spd_voices");
if (!p_spd_open || !p_spd_close || !p_spd_say || !p_spd_pause || !p_spd_resume || !p_spd_stop || !p_spd_set_volume
|| !p_spd_set_voice_rate || !p_spd_set_synthesis_voice || !p_spd_list_synthesis_voices || !p_free_spd_voices) {
mprintf(("Speech: Unable to load one or more symbols from libspeechd.so: %s\n", dlerror()));
dlclose(lib_handle);
lib_handle = nullptr;
return false;
}
return true;
}
// Speech handling starts here
static bool Speech_init = false;
static SPDConnection* spd = nullptr;
bool speech_init()
{
if (Speech_init) {
return true;
}
if (!ensure_speechd_lib()) {
return false;
}
spd = p_spd_open("freespace_open", "main", nullptr, SPD_MODE_SINGLE);
if (!spd) {
mprintf(("Speech: Unable to connect to speech-dispatcher\n"));
if (lib_handle) {
dlclose(lib_handle);
lib_handle = nullptr;
}
return false;
}
Speech_init = true;
return true;
}
void speech_deinit()
{
if ( !Speech_init ) {
return;
}
p_spd_close(spd);
Speech_init = false;
spd = nullptr;
if (lib_handle) {
dlclose(lib_handle);
lib_handle = nullptr;
}
}
bool speech_play(const SCP_string& text)
{
if ( !Speech_init ) {
return false;
}
if (text.empty()) {
nprintf(("Speech", "Not playing speech because passed text is empty.\n"));
return false;
}
return (p_spd_say(spd, SPD_TEXT, text.c_str()) >= 0);
}
bool speech_pause()
{
if ( !Speech_init ) {
return false;
}
p_spd_pause(spd);
return true;
}
bool speech_resume()
{
if ( !Speech_init ) {
return false;
}
p_spd_resume(spd);
return true;
}
bool speech_stop()
{
if ( !Speech_init ) {
return false;
}
p_spd_stop(spd);
return true;
}
bool speech_set_volume(unsigned short volume)
{
if ( !Speech_init ) {
return false;
}
p_spd_set_volume(spd, volume);
return true;
}
bool speech_set_voice(int voice)
{
if ( !Speech_init ) {
return false;
}
auto voices = speech_enumerate_voices();
if (voice < 0 || static_cast<size_t>(voice) >= voices.size()) {
return false;
}
p_spd_set_synthesis_voice(spd, voices[voice].second.c_str());
return true;
}
bool speech_set_rate(float rate_percent)
{
if (!Speech_init) {
return false;
}
// 50 / +150 -> 100 = normal -> range -100 / +100
auto rate = static_cast<signed int>(rate_percent - 100.0f);
CAP(rate, -100, 100);
p_spd_set_voice_rate(spd, rate);
return true;
}
/*
TODO: Try to implement this in some way,
there is no simple way to do it.
*/
bool speech_is_speaking()
{
if ( !Speech_init ) {
return false;
}
return false;
}
SCP_vector<std::pair<int, SCP_string>> speech_enumerate_voices()
{
SCP_vector<std::pair<int, SCP_string>> fsoVoices;
if (!Speech_init) {
if (!ensure_speechd_lib()) {
return fsoVoices;
}
spd = p_spd_open("freespace_open", "main", nullptr, SPD_MODE_SINGLE);
if (!spd) {
mprintf(("Speech: Unable to connect to speech-dispatcher\n"));
if (lib_handle) {
dlclose(lib_handle);
lib_handle = nullptr;
}
return fsoVoices;
}
}
SPDVoice** voices = p_spd_list_synthesis_voices(spd);
if (voices) {
for (int i = 0; voices[i] != nullptr; i++) {
fsoVoices.emplace_back(std::make_pair(i, voices[i]->name));
}
p_free_spd_voices(voices);
}
else {
mprintf(("Speech: Unable to get voice list from speech-dispatcher.\n"));
}
if (!Speech_init) {
p_spd_close(spd);
spd = nullptr;
if (lib_handle) {
dlclose(lib_handle);
lib_handle = nullptr;
}
}
return fsoVoices;
}
#endif