mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* android build support * Update es_compatibility.h * Support reporting flags in android * include pref_path in json v2 * add vulkan headers * shaderc includes * remove headers * force use prebuilt vulkan * fix vulkan inverted screen * fix find sdl3 precompiled android * Update CMakeLists.txt * replace sdl2 android function names for sdl3 versions * change sdl includes in osapi * fix sdl3 includes, again * rename function to sdl3 name * remove unused definition * copy libshaderc_shared.so to output in android * do not look for vulkan-loader in android * revert changes to SDL2.cmake * add comment to vulkan pretransform setup * cleanup maxsize function * rename new android functions in osapi * android trap back button * Remove calculate_max_resize_max_size * touch ui opacity control * change opacity control to int * fix opacity slider * Corrected android working folder fallback and set borderless mode as default * Implement shared helper for JNI calls * Add version tag and sha256 to android prebuilt libs * vulkan android check eSuboptimal * An already requested rebuild also avoids redundant Android surface queries * add cache bool to cmakelist * set string ids
297 lines
8.0 KiB
C++
297 lines
8.0 KiB
C++
#ifdef FS2_SPEECH
|
|
#ifdef __ANDROID__
|
|
#include "globalincs/pstypes.h"
|
|
#include "utils/unicode.h"
|
|
#include "speech.h"
|
|
#include <jni.h>
|
|
#include <SDL3/SDL_system.h>
|
|
#include <SDL3/SDL.h>
|
|
|
|
bool Speech_init = false;
|
|
static jclass j_game_class = nullptr;
|
|
static jmethodID tts_speak = nullptr;
|
|
static jmethodID tts_stop = nullptr;
|
|
static jmethodID tts_pause = nullptr;
|
|
static jmethodID tts_resume = nullptr;
|
|
static jmethodID tts_isSpeaking = nullptr;
|
|
static jmethodID tts_shutdown = nullptr;
|
|
static jmethodID tts_setRate = nullptr;
|
|
static jmethodID tts_setVoice = nullptr;
|
|
static jmethodID tts_getVoices = nullptr;
|
|
|
|
// Helper to get a static method from a java class and clear the exception
|
|
// if the method is not found. This is needed to avoid crashing on the next JNI request.
|
|
static jmethodID get_static_method(JNIEnv* e, jclass cls, const char* name, const char* sig)
|
|
{
|
|
jmethodID m = e->GetStaticMethodID(cls, name, sig);
|
|
if (e->ExceptionCheck()) {
|
|
e->ExceptionClear();
|
|
m = nullptr;
|
|
mprintf(("Speech : Method: %s. Not found on GameActivity! Signature: %s. \n", name, sig));
|
|
}
|
|
return m;
|
|
}
|
|
|
|
// Ask SDL for the JNI Environment and hook to
|
|
// the external TTSManager on the android side of things.
|
|
// Then assign all method IDs for later use.
|
|
bool speech_init()
|
|
{
|
|
Speech_init = false;
|
|
mprintf(("Speech : Try to init TTSManager on GameActivity...\n"));
|
|
|
|
// Get the JNI Environment pointer and current Activity instance via SDL
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
jobject activity = (jobject)SDL_GetAndroidActivity();
|
|
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
if (activity == nullptr) {
|
|
mprintf(("Speech : Unable to get SDL Android activity!\n"));
|
|
return false;
|
|
}
|
|
|
|
// GetObjectClass returns a local ref — promote to global so it survives
|
|
// across JNI calls made from different scopes/threads.
|
|
jclass local_class = env->GetObjectClass(activity);
|
|
env->DeleteLocalRef(activity);
|
|
|
|
if (local_class == nullptr) {
|
|
mprintf(("Speech : Unable to find the GameActivity class!\n"));
|
|
return false;
|
|
}
|
|
|
|
j_game_class = (jclass)env->NewGlobalRef(local_class);
|
|
env->DeleteLocalRef(local_class);
|
|
|
|
// Map all static methods from TTSManager
|
|
tts_speak = get_static_method (env, j_game_class, "tts_speak", "(Ljava/lang/String;)Z");
|
|
tts_stop = get_static_method (env, j_game_class, "tts_stop", "()Z");
|
|
tts_pause = get_static_method (env, j_game_class, "tts_pause", "()Z");
|
|
tts_resume = get_static_method (env, j_game_class, "tts_resume", "()Z");
|
|
tts_isSpeaking = get_static_method (env, j_game_class, "tts_isSpeaking", "()Z");
|
|
tts_shutdown = get_static_method (env, j_game_class, "tts_shutdown", "()V");
|
|
tts_setRate = get_static_method (env, j_game_class, "tts_setRate", "(F)V");
|
|
tts_setVoice = get_static_method (env, j_game_class, "tts_setLanguageTag", "(Ljava/lang/String;)V");
|
|
tts_getVoices = get_static_method (env, j_game_class, "tts_getAvailableLanguageTags", "()[Ljava/lang/String;");
|
|
|
|
if (!tts_speak || !tts_stop || !tts_pause || !tts_resume || !tts_isSpeaking || !tts_shutdown || !tts_setRate || !tts_setVoice || !tts_getVoices) {
|
|
mprintf(("Speech : Unable to map at least one core TTS method to GameActivity!\n"));
|
|
env->DeleteGlobalRef(j_game_class);
|
|
j_game_class = nullptr;
|
|
return false;
|
|
}
|
|
|
|
mprintf(("Speech : Init Completed!\n"));
|
|
Speech_init = true;
|
|
return true;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
jstring j_txt = env->NewStringUTF(text.c_str());
|
|
jboolean ok = env->CallStaticBooleanMethod(j_game_class, tts_speak, j_txt);
|
|
env->DeleteLocalRef(j_txt);
|
|
|
|
if (ok != JNI_TRUE) {
|
|
mprintf(("Speech : Error playing TTS string!\n"));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool speech_stop()
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
return env->CallStaticBooleanMethod(j_game_class, tts_stop) == JNI_TRUE;
|
|
}
|
|
|
|
bool speech_pause()
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
return env->CallStaticBooleanMethod(j_game_class, tts_pause) == JNI_TRUE;
|
|
}
|
|
|
|
bool speech_resume()
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
return env->CallStaticBooleanMethod(j_game_class, tts_resume) == JNI_TRUE;
|
|
}
|
|
|
|
bool speech_is_speaking()
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
return env->CallStaticBooleanMethod(j_game_class, tts_isSpeaking) == JNI_TRUE;
|
|
}
|
|
|
|
void speech_deinit()
|
|
{
|
|
if (!Speech_init)
|
|
return;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env != nullptr) {
|
|
env->CallStaticVoidMethod(j_game_class, tts_shutdown);
|
|
env->DeleteGlobalRef(j_game_class);
|
|
}
|
|
j_game_class = nullptr;
|
|
tts_speak = nullptr;
|
|
tts_stop = nullptr;
|
|
tts_pause = nullptr;
|
|
tts_resume = nullptr;
|
|
tts_isSpeaking = nullptr;
|
|
tts_shutdown = nullptr;
|
|
tts_setRate = nullptr;
|
|
tts_setVoice = nullptr;
|
|
tts_getVoices = nullptr;
|
|
Speech_init = false;
|
|
}
|
|
|
|
|
|
// Android TTS does not expose a direct volume API.
|
|
// Volume is controlled by the STREAM_MUSIC channel at the OS level.
|
|
bool speech_set_volume(unsigned short /*volume*/)
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
bool speech_set_rate(float rate_percent)
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
|
|
if(rate_percent > 150.0)
|
|
rate_percent = 150.0;
|
|
else if(rate_percent < 50.0)
|
|
rate_percent = 50.0;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
float android_rate = rate_percent / 100.0f; // 0.5 .. 1.0 .. 1.5
|
|
env->CallStaticVoidMethod(j_game_class, tts_setRate, (jfloat)android_rate);
|
|
return true;
|
|
}
|
|
|
|
bool speech_set_voice(int voice)
|
|
{
|
|
if (!Speech_init)
|
|
return false;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return false;
|
|
}
|
|
|
|
jobjectArray tags = (jobjectArray)env->CallStaticObjectMethod(j_game_class, tts_getVoices);
|
|
if (tags == nullptr)
|
|
return false;
|
|
|
|
jsize count = env->GetArrayLength(tags);
|
|
if (voice < 0 || voice >= (int)count) {
|
|
env->DeleteLocalRef(tags);
|
|
return false;
|
|
}
|
|
|
|
jstring tag = (jstring)env->GetObjectArrayElement(tags, (jsize)voice);
|
|
env->CallStaticVoidMethod(j_game_class, tts_setVoice, tag);
|
|
env->DeleteLocalRef(tag);
|
|
env->DeleteLocalRef(tags);
|
|
return true;
|
|
}
|
|
|
|
SCP_vector<std::pair<int, SCP_string>> speech_enumerate_voices()
|
|
{
|
|
SCP_vector<std::pair<int, SCP_string>> voices;
|
|
|
|
if (!Speech_init)
|
|
return voices;
|
|
|
|
JNIEnv* env = (JNIEnv*)SDL_GetAndroidJNIEnv();
|
|
if (env == nullptr) {
|
|
mprintf(("Speech : Unable to get JNI environment!\n"));
|
|
return voices;
|
|
}
|
|
|
|
jobjectArray tags = (jobjectArray)env->CallStaticObjectMethod(j_game_class, tts_getVoices);
|
|
if (tags == nullptr)
|
|
return voices;
|
|
|
|
jsize count = env->GetArrayLength(tags);
|
|
|
|
for (jsize i = 0; i < count; ++i) {
|
|
jstring tag = (jstring)env->GetObjectArrayElement(tags, i);
|
|
if (tag == nullptr)
|
|
continue;
|
|
|
|
const char* raw = env->GetStringUTFChars(tag, nullptr);
|
|
if (raw) {
|
|
voices.emplace_back((int)i, SCP_string(raw));
|
|
env->ReleaseStringUTFChars(tag, raw);
|
|
}
|
|
env->DeleteLocalRef(tag);
|
|
}
|
|
|
|
env->DeleteLocalRef(tags);
|
|
return voices;
|
|
}
|
|
|
|
#endif // __ANDROID__
|
|
#endif // FS2_SPEECH
|