Skip to content

Commit e157273

Browse files
committed
add tts support
1 parent cc2135e commit e157273

4 files changed

Lines changed: 238 additions & 1 deletion

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ IF(RESET_INSTALL_PREFIX)
8383
ENDIF(NOT $ENV{FS2PATH} STREQUAL "")
8484
ENDIF(RESET_INSTALL_PREFIX)
8585

86-
IF(WIN32 OR APPLE OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
86+
IF(WIN32 OR APPLE OR ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
8787
OPTION(FSO_USE_SPEECH "Use text-to-speach libraries" ON)
8888
ELSE()
8989
OPTION(FSO_USE_SPEECH "Use text-to-speach libraries" OFF)

cmake/finder/FindSpeech.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ if (WIN32)
1111
endif()
1212
elseif(APPLE)
1313
# it should just work
14+
elseif(ANDROID)
15+
# connects to a java TTS manager via SDL activity class
1416
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
1517
# uses speech-dispatcher with dlopen
1618
else()

code/sound/speech_android.cpp

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#ifdef FS2_SPEECH
2+
#ifdef __ANDROID__
3+
#include "globalincs/pstypes.h"
4+
#include "utils/unicode.h"
5+
#include "speech.h"
6+
#include <jni.h>
7+
#include "SDL.h"
8+
#include "SDL_system.h"
9+
10+
bool Speech_init = false;
11+
static JNIEnv* env = nullptr;
12+
static jobject j_game_class = nullptr;
13+
static jmethodID tts_speak = nullptr;
14+
static jmethodID tts_stop = nullptr;
15+
static jmethodID tts_pause = nullptr;
16+
static jmethodID tts_resume = nullptr;
17+
static jmethodID tts_isSpeaking = nullptr;
18+
static jmethodID tts_shutdown = nullptr;
19+
static jmethodID tts_setRate = nullptr;
20+
static jmethodID tts_setVoice = nullptr;
21+
static jmethodID tts_getVoices = nullptr;
22+
23+
// Ask SDL for the JNI Environment and hook to
24+
// the external TTSManager on the android side of things.
25+
// Then assign all method IDs for later use.
26+
bool speech_init()
27+
{
28+
Speech_init = false;
29+
mprintf(("Speech : Try to init TTSManager on GameActivity...\n"));
30+
31+
// Get the JNI Environment pointer and current Activity instance via SDL
32+
env = (JNIEnv*)SDL_AndroidGetJNIEnv();
33+
jobject activity = (jobject)SDL_AndroidGetActivity();
34+
35+
if (env == nullptr) {
36+
mprintf(("Speech : Unable to get JNI environment!\n"));
37+
return false;
38+
}
39+
40+
if (activity == nullptr) {
41+
mprintf(("Speech : Unable to get SDL Android activity!\n"));
42+
return false;
43+
}
44+
45+
// GetObjectClass returns a local ref — promote to global so it survives
46+
// across JNI calls made from different scopes/threads.
47+
jclass local_class = env->GetObjectClass(activity);
48+
env->DeleteLocalRef(activity);
49+
50+
if (local_class == nullptr) {
51+
mprintf(("Speech : Unable to find the GameActivity class!\n"));
52+
return false;
53+
}
54+
55+
j_game_class = (jclass)env->NewGlobalRef(local_class);
56+
env->DeleteLocalRef(local_class);
57+
58+
// Map all static methods from TTSManager
59+
tts_speak = env->GetStaticMethodID(j_game_class, "tts_speak", "(Ljava/lang/String;)Z");
60+
tts_stop = env->GetStaticMethodID(j_game_class, "tts_stop", "()Z");
61+
tts_pause = env->GetStaticMethodID(j_game_class, "tts_pause", "()Z");
62+
tts_resume = env->GetStaticMethodID(j_game_class, "tts_resume", "()Z");
63+
tts_isSpeaking = env->GetStaticMethodID(j_game_class, "tts_isSpeaking", "()Z");
64+
tts_shutdown = env->GetStaticMethodID(j_game_class, "tts_shutdown", "()V");
65+
tts_setRate = env->GetStaticMethodID(j_game_class, "tts_setRate", "(F)V");
66+
tts_setVoice = env->GetStaticMethodID(j_game_class, "tts_setLanguageTag", "(Ljava/lang/String;)V");
67+
tts_getVoices = env->GetStaticMethodID(j_game_class, "tts_getAvailableLanguageTags", "()[Ljava/lang/String;");
68+
69+
if (!tts_speak || !tts_stop || !tts_pause || !tts_resume || !tts_isSpeaking || !tts_shutdown || !tts_setRate || !tts_setVoice || !tts_getVoices) {
70+
mprintf(("Speech : Unable to map at least one core TTS method to GameActivity!\n"));
71+
env->DeleteGlobalRef(j_game_class);
72+
j_game_class = nullptr;
73+
return false;
74+
}
75+
76+
mprintf(("Speech : Init Completed!\n"));
77+
Speech_init = true;
78+
return true;
79+
}
80+
81+
82+
bool speech_play(const SCP_string& text)
83+
{
84+
if (!Speech_init)
85+
return false;
86+
87+
if (text.empty()) {
88+
nprintf(("Speech", "Not playing speech because passed text is empty.\n"));
89+
return false;
90+
}
91+
92+
jstring j_txt = env->NewStringUTF(text.c_str());
93+
mprintf(("Speech : Playing TTS string: %s!\n", text.c_str()));
94+
jboolean ok = env->CallStaticBooleanMethod(j_game_class, tts_speak, j_txt);
95+
env->DeleteLocalRef(j_txt);
96+
97+
if (ok != JNI_TRUE) {
98+
mprintf(("Speech : Error playing TTS string!\n"));
99+
return false;
100+
}
101+
return true;
102+
}
103+
104+
bool speech_stop()
105+
{
106+
if (!Speech_init)
107+
return false;
108+
return env->CallStaticBooleanMethod(j_game_class, tts_stop) == JNI_TRUE;
109+
}
110+
111+
bool speech_pause()
112+
{
113+
if (!Speech_init)
114+
return false;
115+
return env->CallStaticBooleanMethod(j_game_class, tts_pause) == JNI_TRUE;
116+
}
117+
118+
bool speech_resume()
119+
{
120+
if (!Speech_init)
121+
return false;
122+
return env->CallStaticBooleanMethod(j_game_class, tts_resume) == JNI_TRUE;
123+
}
124+
125+
bool speech_is_speaking()
126+
{
127+
if (!Speech_init)
128+
return false;
129+
return env->CallStaticBooleanMethod(j_game_class, tts_isSpeaking) == JNI_TRUE;
130+
}
131+
132+
void speech_deinit()
133+
{
134+
if (!Speech_init)
135+
return;
136+
env->CallStaticVoidMethod(j_game_class, tts_shutdown);
137+
env->DeleteGlobalRef(j_game_class);
138+
j_game_class = nullptr;
139+
tts_speak = nullptr;
140+
tts_stop = nullptr;
141+
tts_pause = nullptr;
142+
tts_resume = nullptr;
143+
tts_isSpeaking = nullptr;
144+
tts_shutdown = nullptr;
145+
tts_setRate = nullptr;
146+
tts_setVoice = nullptr;
147+
tts_getVoices = nullptr;
148+
Speech_init = false;
149+
}
150+
151+
152+
// Android TTS does not expose a direct volume API.
153+
// Volume is controlled by the STREAM_MUSIC channel at the OS level.
154+
bool speech_set_volume(unsigned short /*volume*/)
155+
{
156+
if (!Speech_init)
157+
return false;
158+
return true;
159+
}
160+
161+
bool speech_set_rate(float rate_percent)
162+
{
163+
if (!Speech_init)
164+
return false;
165+
166+
if(rate_percent > 150.0)
167+
rate_percent = 150.0;
168+
else if(rate_percent < 50.0)
169+
rate_percent = 50.0;
170+
171+
float android_rate = rate_percent / 100.0f; // 0.5 .. 1.0 .. 1.5
172+
env->CallStaticVoidMethod(j_game_class, tts_setRate, (jfloat)android_rate);
173+
return true;
174+
}
175+
176+
bool speech_set_voice(int voice)
177+
{
178+
if (!Speech_init)
179+
return false;
180+
181+
jobjectArray tags = (jobjectArray)env->CallStaticObjectMethod(j_game_class, tts_getVoices);
182+
if (tags == nullptr)
183+
return false;
184+
185+
jsize count = env->GetArrayLength(tags);
186+
if (voice < 0 || voice >= (int)count) {
187+
env->DeleteLocalRef(tags);
188+
return false;
189+
}
190+
191+
jstring tag = (jstring)env->GetObjectArrayElement(tags, (jsize)voice);
192+
env->CallStaticVoidMethod(j_game_class, tts_setVoice, tag);
193+
env->DeleteLocalRef(tag);
194+
env->DeleteLocalRef(tags);
195+
return true;
196+
}
197+
198+
SCP_vector<std::pair<int, SCP_string>> speech_enumerate_voices()
199+
{
200+
SCP_vector<std::pair<int, SCP_string>> voices;
201+
202+
if (!Speech_init)
203+
return voices;
204+
205+
jobjectArray tags = (jobjectArray)env->CallStaticObjectMethod(j_game_class, tts_getVoices);
206+
if (tags == nullptr)
207+
return voices;
208+
209+
jsize count = env->GetArrayLength(tags);
210+
voices.reserve((size_t)count);
211+
212+
for (jsize i = 0; i < count; ++i) {
213+
jstring tag = (jstring)env->GetObjectArrayElement(tags, i);
214+
if (tag == nullptr)
215+
continue;
216+
217+
const char* raw = env->GetStringUTFChars(tag, nullptr);
218+
if (raw) {
219+
voices.emplace_back((int)i, SCP_string(raw));
220+
env->ReleaseStringUTFChars(tag, raw);
221+
}
222+
env->DeleteLocalRef(tag);
223+
}
224+
225+
env->DeleteLocalRef(tags);
226+
return voices;
227+
}
228+
229+
#endif // __ANDROID__
230+
#endif // FS2_SPEECH

code/source_groups.cmake

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,11 @@ elseif (APPLE)
16391639
${file_root_sound}
16401640
sound/speech_mac.mm
16411641
)
1642+
elseif (ANDROID)
1643+
add_file_folder("Sound"
1644+
${file_root_sound}
1645+
sound/speech_android.cpp
1646+
)
16421647
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
16431648
add_file_folder("Sound"
16441649
${file_root_sound}

0 commit comments

Comments
 (0)