diff --git a/.github/workflows/apt-deps.txt b/.github/workflows/apt-deps.txt index 51e9574f8..3461cdbac 100644 --- a/.github/workflows/apt-deps.txt +++ b/.github/workflows/apt-deps.txt @@ -1 +1 @@ -libusb-dev libusb-1.0-0-dev libsdl2-dev libsdl2-net-dev libpng-dev libglew-dev nlohmann-json3-dev libtinyxml2-dev libspdlog-dev ninja-build +libusb-dev libusb-1.0-0-dev libsdl2-dev libsdl2-net-dev libpng-dev libglew-dev nlohmann-json3-dev libtinyxml2-dev libspdlog-dev libespeak-ng-dev ninja-build diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index 7277b2dfb..07109a918 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -141,16 +141,21 @@ endif() # handle Network removals if (!BUILD_REMOTE_CONTROL) - list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/crowd-control/*") + list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/crowd-control/") endif() # handle speechsynthesizer removals if (CMAKE_SYSTEM_NAME STREQUAL "Windows") - list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/Darwin*") + list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/Darwin") elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") - list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/SAPI*") + list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/SAPI") else() - list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/(Darwin|SAPI).*") + list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/(Darwin|SAPI)") +endif() + +find_library(ESPEAK espeak-ng) +if (NOT ESPEAK) + list(FILTER soh__ EXCLUDE REGEX "soh/Enhancements/speechsynthesizer/ESpeak") endif() # soh/Extractor {{{ @@ -176,12 +181,12 @@ file(GLOB_RECURSE src__ RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.c" "src/*.h" set_source_files_properties(${src__} PROPERTIES COMPILE_OPTIONS "${WARNING_OVERRIDE}") list(APPEND src__ ${CMAKE_CURRENT_SOURCE_DIR}/Resource.rc) -list(FILTER src__ EXCLUDE REGEX "src/dmadata/*") -list(FILTER src__ EXCLUDE REGEX "src/elf_message/*") -list(FILTER src__ EXCLUDE REGEX "src/libultra/io/*") -list(FILTER src__ EXCLUDE REGEX "src/libultra/libc/*") -list(FILTER src__ EXCLUDE REGEX "src/libultra/os/*") -list(FILTER src__ EXCLUDE REGEX "src/libultra/rmon/*") +list(FILTER src__ EXCLUDE REGEX "src/dmadata/") +list(FILTER src__ EXCLUDE REGEX "src/elf_message/") +list(FILTER src__ EXCLUDE REGEX "src/libultra/io/") +list(FILTER src__ EXCLUDE REGEX "src/libultra/libc/") +list(FILTER src__ EXCLUDE REGEX "src/libultra/os/") +list(FILTER src__ EXCLUDE REGEX "src/libultra/rmon/") list(APPEND src__ "src/libultra/libc/sprintf.c") list(REMOVE_ITEM src__ "src/libultra/gu/cosf.c") list(REMOVE_ITEM src__ "src/libultra/gu/lookat.c") @@ -317,6 +322,10 @@ if (BUILD_REMOTE_CONTROL) endif() endif() +if (ESPEAK) + add_compile_definitions(ESPEAK=1) +endif() + target_include_directories(${PROJECT_NAME} PRIVATE assets ${CMAKE_CURRENT_SOURCE_DIR}/include/ ${CMAKE_CURRENT_SOURCE_DIR}/src/ diff --git a/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp new file mode 100644 index 000000000..f92959ff1 --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.cpp @@ -0,0 +1,49 @@ +#include "ESpeakSpeechSynthesizer.h" +#include +#include + +ESpeakSpeechSynthesizer::ESpeakSpeechSynthesizer() { +} + +bool ESpeakSpeechSynthesizer::DoInit() { + void* espeak = dlopen("libespeak-ng.so", RTLD_LAZY | RTLD_LOCAL); + if (espeak != NULL) { + this->Initialize = (speak_Initialize)dlsym(espeak, "espeak_Initialize"); + this->SetVoiceByProperties = (speak_SetVoiceByProperties)dlsym(espeak, "espeak_SetVoiceByProperties"); + this->Synth = (speak_Synth)dlsym(espeak, "espeak_Synth"); + this->Terminate = (speak_Terminate)dlsym(espeak, "espeak_Terminate"); + if (this->Initialize == NULL || this->SetVoiceByProperties == NULL || this->Synth == NULL || + this->Terminate == NULL) { + lusprintf(__FILE__, __LINE__, 2, "Failed to load espeak-ng"); + dlclose(espeak); + return false; + } else { + this->espeak = espeak; + return this->Initialize(AUDIO_OUTPUT_PLAYBACK, 100, NULL, 0) != -1; + } + } + return true; +} + +void ESpeakSpeechSynthesizer::DoUninitialize() { + if (this->espeak != NULL) { + this->Terminate(); + dlclose(this->espeak); + this->espeak = NULL; + } +} + +void ESpeakSpeechSynthesizer::Speak(const char* text, const char* language) { + if (this->espeak == NULL) { + lusprintf(__FILE__, __LINE__, 2, "Spoken Text (%s): %s", language, text); + } else { + if (language != this->mLanguage) { + espeak_VOICE voice = { .languages = language }; + if (this->SetVoiceByProperties(&voice)) { + return; + } + this->mLanguage = language; + } + this->Synth(text, 100, 0, POS_CHARACTER, 0, espeakCHARS_UTF8, NULL, NULL); + } +} diff --git a/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h new file mode 100644 index 000000000..0844b9293 --- /dev/null +++ b/soh/soh/Enhancements/speechsynthesizer/ESpeakSpeechSynthesizer.h @@ -0,0 +1,34 @@ +#pragma once + +#include "SpeechSynthesizer.h" + +extern "C" { +#include + +// C23 typeof could help here +typedef ESPEAK_API int (*speak_Initialize)(espeak_AUDIO_OUTPUT output, int buflength, const char* path, int options); +typedef ESPEAK_API espeak_ERROR (*speak_Terminate)(void); +typedef ESPEAK_API espeak_ERROR (*speak_SetVoiceByProperties)(espeak_VOICE* voice_spec); +typedef ESPEAK_API espeak_ERROR (*speak_Synth)(const void* text, size_t size, unsigned int position, + espeak_POSITION_TYPE position_type, unsigned int end_position, + unsigned int flags, unsigned int* unique_identifier, void* user_data); +} + +class ESpeakSpeechSynthesizer : public SpeechSynthesizer { + public: + ESpeakSpeechSynthesizer(); + + void Speak(const char* text, const char* language); + + protected: + bool DoInit(void); + void DoUninitialize(void); + + private: + const char* mLanguage = NULL; + void* espeak = NULL; + speak_Initialize Initialize = NULL; + speak_SetVoiceByProperties SetVoiceByProperties = NULL; + speak_Synth Synth = NULL; + speak_Terminate Terminate = NULL; +}; diff --git a/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h b/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h index 29d209b5f..e82da907f 100644 --- a/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h +++ b/soh/soh/Enhancements/speechsynthesizer/SpeechSynthesizer.h @@ -35,6 +35,8 @@ class SpeechSynthesizer { #include "SAPISpeechSynthesizer.h" #elif defined(__APPLE__) #include "DarwinSpeechSynthesizer.h" +#elif ESPEAK +#include "ESpeakSpeechSynthesizer.h" #endif #include "SpeechLogger.h" diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 72030657e..b77426094 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1224,14 +1224,14 @@ extern "C" void InitOTR() { ActorDB::Instance = new ActorDB(); #ifdef __APPLE__ SpeechSynthesizer::Instance = new DarwinSpeechSynthesizer(); - SpeechSynthesizer::Instance->Init(); #elif defined(_WIN32) SpeechSynthesizer::Instance = new SAPISpeechSynthesizer(); - SpeechSynthesizer::Instance->Init(); +#elif ESPEAK + SpeechSynthesizer::Instance = new ESpeakSpeechSynthesizer(); #else SpeechSynthesizer::Instance = new SpeechLogger(); - SpeechSynthesizer::Instance->Init(); #endif + SpeechSynthesizer::Instance->Init(); #ifdef ENABLE_REMOTE_CONTROL CrowdControl::Instance = new CrowdControl(); diff --git a/soh/soh/SohGui/SohMenuSettings.cpp b/soh/soh/SohGui/SohMenuSettings.cpp index 909c647bd..f2e4a8658 100644 --- a/soh/soh/SohGui/SohMenuSettings.cpp +++ b/soh/soh/SohGui/SohMenuSettings.cpp @@ -185,7 +185,7 @@ void SohMenu::AddMenuSettings() { .ComboMap(languages) .DefaultIndex(LANGUAGE_ENG)); AddWidget(path, "Accessibility", WIDGET_SEPARATOR_TEXT); -#if defined(_WIN32) || defined(__APPLE__) +#if defined(_WIN32) || defined(__APPLE__) || defined(ESPEAK) AddWidget(path, "Text to Speech", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_SETTING("A11yTTS")) .RaceDisable(false)