[Scummvm-git-logs] scummvm master -> 9a05ccf9dc85497e6fb3fb8ec4a94573b16dd8ca
lephilousophe
noreply at scummvm.org
Mon Aug 24 14:14:49 UTC 2026
This automated email contains information about 7 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
a04ac5dd8d COMMON: Return unknown voices if there is no requested gender voices
214a35b477 TESTBED: Adapt speech test for unknown gender voices
507e11ce36 TESTBED: Make sure TTS engine is ready before running a test
4aeb8d9184 ANDROID: Update Gradle and Android Gradle Plugin
25904c3122 ANDROID: Make JNI string functions available to the backend
a597b52627 ANDROID: Add a compatibility helper to build Locale
9a05ccf9dc ANDROID: Add TTS backend
Commit: a04ac5dd8d27449c47c9d94d0f08c7825a796e7c
https://github.com/scummvm/scummvm/commit/a04ac5dd8d27449c47c9d94d0f08c7825a796e7c
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
COMMON: Return unknown voices if there is no requested gender voices
Changed paths:
common/text-to-speech.cpp
diff --git a/common/text-to-speech.cpp b/common/text-to-speech.cpp
index 2e22b9777d6..a4f56524731 100644
--- a/common/text-to-speech.cpp
+++ b/common/text-to-speech.cpp
@@ -149,6 +149,14 @@ Array<int> TextToSpeechManager::getVoiceIndicesByGender(TTSVoice::Gender gender)
if (_ttsState->_availableVoices[i].getGender() == gender)
results.push_back(i);
}
+ if (results.size() > 0) {
+ return results;
+ }
+ // If there were no results, add unknown gender ones
+ for (unsigned i = 0; i < _ttsState->_availableVoices.size(); i++) {
+ if (_ttsState->_availableVoices[i].getGender() == TTSVoice::UNKNOWN_GENDER)
+ results.push_back(i);
+ }
return results;
}
Commit: 214a35b4770f0e21fc71251f117bf2ec6831c7fa
https://github.com/scummvm/scummvm/commit/214a35b4770f0e21fc71251f117bf2ec6831c7fa
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
TESTBED: Adapt speech test for unknown gender voices
Changed paths:
engines/testbed/speech.cpp
diff --git a/engines/testbed/speech.cpp b/engines/testbed/speech.cpp
index 2fda729e916..06b6b236ec4 100644
--- a/engines/testbed/speech.cpp
+++ b/engines/testbed/speech.cpp
@@ -52,29 +52,39 @@ TestExitStatus Speechtests::testMale() {
ttsMan->setRate(0);
ttsMan->setPitch(0);
Testsuite::clearScreen();
- Common::String info = "Male voice test. You should expect a male voice to say \"Testing text to speech with male voice.\"";
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing male TTS voice", pt);
- if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
- Testsuite::logPrintf("Info! Skipping test : testMale\n");
- return kTestSkipped;
- }
-
Common::Array<int> maleVoices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::MALE);
if (maleVoices.size() == 0) {
Testsuite::displayMessage("No male voice available");
return kTestFailed;
}
ttsMan->setVoice(maleVoices[0]);
- ttsMan->say("Testing text to speech with male voice.");
+
+ Common::TTSVoice currentVoice = ttsMan->getVoice();
+
+ Common::String gender = "a male";
+ Common::String msg = "Testing text to speech with male voice.";
+ if (currentVoice.getGender() != Common::TTSVoice::MALE) {
+ gender = "an unknown gender";
+ msg = "No male voice was available. Here is an unknown gender voice instead.";
+ }
+
+ Common::String info = Common::String::format("Male voice test. You should expect %s voice to say \"%s\"", gender.c_str(), msg.c_str());
+ if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
+ Testsuite::logPrintf("Info! Skipping test : testMale\n");
+ return kTestSkipped;
+ }
+
+ ttsMan->say(msg);
if (!ttsMan->isSpeaking()) {
Testsuite::logDetailedPrintf("Male TTS failed\n");
return kTestFailed;
}
waitForSpeechEnd(ttsMan);
- Common::String prompt = "Did you hear male voice saying: \"Testing text to speech with male voice.\" ?";
+ Common::String prompt = Common::String::format("Did you hear %s voice saying: \"%s\" ?", gender.c_str(), msg.c_str());
if (!Testsuite::handleInteractiveInput(prompt, "Yes", "No", kOptionLeft)) {
Testsuite::logDetailedPrintf("Male TTS failed\n");
return kTestFailed;
@@ -89,29 +99,39 @@ TestExitStatus Speechtests::testFemale() {
ttsMan->setRate(0);
ttsMan->setPitch(0);
Testsuite::clearScreen();
- Common::String info = "Female voice test. You should expect a female voice to say \"Testing text to speech with female voice.\"";
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing female TTS voice", pt);
- if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
- Testsuite::logPrintf("Info! Skipping test : testFemale\n");
- return kTestSkipped;
- }
-
Common::Array<int> femaleVoices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::FEMALE);
if (femaleVoices.size() == 0) {
- Testsuite::logDetailedPrintf("Female TTS failed\n");
+ Testsuite::displayMessage("No female voice available");
return kTestFailed;
}
ttsMan->setVoice(femaleVoices[0]);
- ttsMan->say("Testing text to speech with female voice.");
+
+ Common::TTSVoice currentVoice = ttsMan->getVoice();
+
+ Common::String gender = "a female";
+ Common::String msg = "Testing text to speech with female voice.";
+ if (currentVoice.getGender() != Common::TTSVoice::FEMALE) {
+ gender = "an unknown gender";
+ msg = "No female voice was available. Here is an unknown gender voice instead.";
+ }
+
+ Common::String info = Common::String::format("Female voice test. You should expect %s voice to say \"%s\"", gender.c_str(), msg.c_str());
+ if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
+ Testsuite::logPrintf("Info! Skipping test : testFemale\n");
+ return kTestSkipped;
+ }
+
+ ttsMan->say(msg);
if (!ttsMan->isSpeaking()) {
Testsuite::logDetailedPrintf("Female TTS failed\n");
return kTestFailed;
}
waitForSpeechEnd(ttsMan);
- Common::String prompt = "Did you hear female voice saying: \"Testing text to speech with female voice.\" ?";
+ Common::String prompt = Common::String::format("Did you hear %s voice saying: \"%s\" ?", gender.c_str(), msg.c_str());
if (!Testsuite::handleInteractiveInput(prompt, "Yes", "No", kOptionLeft)) {
Testsuite::logDetailedPrintf("Female TTS failed\n");
return kTestFailed;
Commit: 507e11ce36989553ae620795631dc7117ac111f5
https://github.com/scummvm/scummvm/commit/507e11ce36989553ae620795631dc7117ac111f5
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
TESTBED: Make sure TTS engine is ready before running a test
Changed paths:
engines/testbed/speech.cpp
diff --git a/engines/testbed/speech.cpp b/engines/testbed/speech.cpp
index 06b6b236ec4..ce6b97e552c 100644
--- a/engines/testbed/speech.cpp
+++ b/engines/testbed/speech.cpp
@@ -56,6 +56,12 @@ TestExitStatus Speechtests::testMale() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing male TTS voice", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
Common::Array<int> maleVoices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::MALE);
if (maleVoices.size() == 0) {
Testsuite::displayMessage("No male voice available");
@@ -103,6 +109,12 @@ TestExitStatus Speechtests::testFemale() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing female TTS voice", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
Common::Array<int> femaleVoices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::FEMALE);
if (femaleVoices.size() == 0) {
Testsuite::displayMessage("No female voice available");
@@ -152,6 +164,12 @@ TestExitStatus Speechtests::testStop() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS stop", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testStop\n");
return kTestSkipped;
@@ -188,6 +206,12 @@ TestExitStatus Speechtests::testStopAndSpeak() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS stop and speak", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testStop\n");
return kTestSkipped;
@@ -225,6 +249,12 @@ TestExitStatus Speechtests::testPauseResume() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS pause", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testPauseResume\n");
return kTestSkipped;
@@ -270,6 +300,12 @@ TestExitStatus Speechtests::testRate() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS rate", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testRate\n");
return kTestSkipped;
@@ -303,6 +339,12 @@ TestExitStatus Speechtests::testVolume() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS volume", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testVolume\n");
return kTestSkipped;
@@ -335,6 +377,12 @@ TestExitStatus Speechtests::testPitch() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS pitch", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testPitch\n");
return kTestSkipped;
@@ -367,6 +415,12 @@ TestExitStatus Speechtests::testStateStacking() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS state stacking", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testStateStacking\n");
return kTestSkipped;
@@ -421,6 +475,12 @@ TestExitStatus Speechtests::testQueueing() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS queue", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testQueueing\n");
return kTestSkipped;
@@ -450,6 +510,12 @@ TestExitStatus Speechtests::testInterrupting() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS interrupt", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testInterrupting\n");
return kTestSkipped;
@@ -480,6 +546,12 @@ TestExitStatus Speechtests::testDroping() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS drop", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testDroping\n");
return kTestSkipped;
@@ -509,6 +581,12 @@ TestExitStatus Speechtests::testInterruptNoRepeat() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS Interrupt No Repeat", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testInterruptNoRepeat\n");
return kTestSkipped;
@@ -546,6 +624,12 @@ TestExitStatus Speechtests::testQueueNoRepeat() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS Queue No Repeat", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testQueueNoRepeat\n");
return kTestSkipped;
@@ -581,6 +665,12 @@ TestExitStatus Speechtests::testQueueEmptyString() {
Common::Point pt(0, 100);
Testsuite::writeOnScreen("Testing TTS Queue No Repeat", pt);
+ waitForSpeechEnd(ttsMan);
+ if (!ttsMan->isReady()) {
+ Testsuite::logDetailedPrintf("TTS engine is not ready\n");
+ return kTestFailed;
+ }
+
if (Testsuite::handleInteractiveInput(info, "OK", "Skip", kOptionRight)) {
Testsuite::logPrintf("Info! Skipping test : testQueueNoRepeat\n");
return kTestSkipped;
Commit: 4aeb8d91840edff423e40e796edfed59e023a81d
https://github.com/scummvm/scummvm/commit/4aeb8d91840edff423e40e796edfed59e023a81d
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
ANDROID: Update Gradle and Android Gradle Plugin
Changed paths:
dists/android/build.gradle
dists/android/gradle/wrapper/gradle-wrapper.properties
diff --git a/dists/android/build.gradle b/dists/android/build.gradle
index c938b6168df..cfe66a0c0e1 100644
--- a/dists/android/build.gradle
+++ b/dists/android/build.gradle
@@ -1,5 +1,5 @@
plugins {
- id('com.android.application') version '9.2.1'
+ id('com.android.application') version '9.3.1'
}
// Load our source dependent properties
diff --git a/dists/android/gradle/wrapper/gradle-wrapper.properties b/dists/android/gradle/wrapper/gradle-wrapper.properties
index a9db11550c6..ad7845be306 100644
--- a/dists/android/gradle/wrapper/gradle-wrapper.properties
+++ b/dists/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Commit: 25904c3122a93bf41d44f5e968e8316d611d3a84
https://github.com/scummvm/scummvm/commit/25904c3122a93bf41d44f5e968e8316d611d3a84
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
ANDROID: Make JNI string functions available to the backend
Changed paths:
backends/platform/android/jni-android.h
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index 086bb7d0537..d8932ca806a 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -122,6 +122,8 @@ public:
static int exportBackup(const Common::U32String &prompt);
static int importBackup(const Common::U32String &prompt, const Common::String &path);
+ static jstring convertToJString(JNIEnv *env, const Common::U32String &str);
+ static Common::U32String convertFromJString(JNIEnv *env, const jstring &jstr);
private:
static pthread_key_t _env_tls;
@@ -197,8 +199,6 @@ private:
static void notifyAudioDisconnect(JNIEnv *env, jclass clazz);
static jstring getNativeVersionInfo(JNIEnv *env, jobject self);
- static jstring convertToJString(JNIEnv *env, const Common::U32String &str);
- static Common::U32String convertFromJString(JNIEnv *env, const jstring &jstr);
static JNIEnv *fetchEnv();
static int fetchEGLVersion();
Commit: a597b52627dd84439bb13bdd3bdd6d4bb6ada075
https://github.com/scummvm/scummvm/commit/a597b52627dd84439bb13bdd3bdd6d4bb6ada075
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
ANDROID: Add a compatibility helper to build Locale
Changed paths:
backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
diff --git a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
index e4b8a63c43d..a823c5050c4 100644
--- a/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
+++ b/backends/platform/android/org/scummvm/scummvm/CompatHelpers.java
@@ -51,6 +51,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Objects;
class CompatHelpers {
@@ -565,4 +566,30 @@ class CompatHelpers {
}
}
}
+
+ static class LocaleCompat {
+ public static Locale buildLocale(String language) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ return LocaleCompatLollipop.buildLocale(language);
+ } else {
+ return LocaleCompatOld.buildLocale(language);
+ }
+ }
+
+ @RequiresApi(api = android.os.Build.VERSION_CODES.LOLLIPOP)
+ private static class LocaleCompatLollipop {
+ public static Locale buildLocale(String language) {
+ Locale.Builder builder = new Locale.Builder();
+ builder.setLanguage(language);
+ return builder.build();
+ }
+ }
+
+ @SuppressWarnings({"deprecation", "RedundantSuppression"})
+ private static class LocaleCompatOld {
+ public static Locale buildLocale(String language) {
+ return new Locale(language);
+ }
+ }
+ }
}
Commit: 9a05ccf9dc85497e6fb3fb8ec4a94573b16dd8ca
https://github.com/scummvm/scummvm/commit/9a05ccf9dc85497e6fb3fb8ec4a94573b16dd8ca
Author: Le Philousophe (lephilousophe at users.noreply.github.com)
Date: 2026-08-24T16:14:42+02:00
Commit Message:
ANDROID: Add TTS backend
Changed paths:
A backends/platform/android/org/scummvm/scummvm/TextToSpeechManager.java
A backends/text-to-speech/android/android-text-to-speech.cpp
A backends/text-to-speech/android/android-text-to-speech.h
backends/module.mk
backends/platform/android/android.cpp
backends/platform/android/jni-android.cpp
backends/platform/android/jni-android.h
backends/platform/android/org/scummvm/scummvm/ScummVM.java
backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
configure
dists/android.strings.xml.cpp
dists/android/AndroidManifest.xml
dists/android/res/values/strings.xml
diff --git a/backends/module.mk b/backends/module.mk
index 8effe44930b..b3b8a5a0028 100644
--- a/backends/module.mk
+++ b/backends/module.mk
@@ -367,6 +367,11 @@ MODULE_OBJS += \
networking/http/android/networkreadstream-android.o
endif
+ifdef USE_TTS
+MODULE_OBJS += \
+ text-to-speech/android/android-text-to-speech.o
+endif
+
# Oboe headers need C++14...
$(MODULE)/mixer/android/android-mixer.o: CXXFLAGS += "-std=c++14"
diff --git a/backends/platform/android/android.cpp b/backends/platform/android/android.cpp
index f4e620081d0..5ed1679fe0d 100644
--- a/backends/platform/android/android.cpp
+++ b/backends/platform/android/android.cpp
@@ -71,6 +71,7 @@
#include "backends/mixer/mixer.h"
#include "backends/mutex/pthread/pthread-mutex.h"
#include "backends/saves/default/default-saves.h"
+#include "backends/text-to-speech/android/android-text-to-speech.h"
#include "backends/timer/default/default-timer.h"
#include "backends/keymapper/keymapper.h"
@@ -255,6 +256,9 @@ OSystem_Android::~OSystem_Android() {
delete _savefileManager;
_savefileManager = 0;
+ delete _textToSpeechManager;
+ _textToSpeechManager = 0;
+
// Uninitialize graphics manager now to avoid it to be done later when touch controls are destroyed
delete _graphicsManager;
_graphicsManager = 0;
@@ -436,6 +440,16 @@ void OSystem_Android::initBackend() {
_graphicsManager = new AndroidGraphicsManager();
+#ifdef USE_TTS
+ // Initialize Text to Speech manager
+ _textToSpeechManager = new AndroidTextToSpeechManager();
+#ifdef USE_TRANSLATION
+ _textToSpeechManager->setLanguage(TransMan.getCurrentLanguage());
+#else
+ _textToSpeechManager->setLanguage("en");
+#endif
+#endif
+
// renice this thread to boost the audio thread
if (setpriority(PRIO_PROCESS, 0, 19) < 0)
warning("couldn't renice the main thread");
diff --git a/backends/platform/android/jni-android.cpp b/backends/platform/android/jni-android.cpp
index ef47360306c..a954f2c7363 100644
--- a/backends/platform/android/jni-android.cpp
+++ b/backends/platform/android/jni-android.cpp
@@ -102,6 +102,7 @@ jmethodID JNI::_MID_getScummVMConfigPath;
jmethodID JNI::_MID_getScummVMLogPath;
jmethodID JNI::_MID_setCurrentGame = 0;
jmethodID JNI::_MID_notifyHTTPService = 0;
+jmethodID JNI::_MID_getTTSManager = 0;
jmethodID JNI::_MID_getSysArchives = 0;
jmethodID JNI::_MID_getAllStorageLocations = 0;
jmethodID JNI::_MID_initSurface = 0;
@@ -596,6 +597,21 @@ void JNI::notifyHTTPService(int localPort, bool minimal) {
}
}
+jobject JNI::getTTSManager() {
+ JNIEnv *env = JNI::getEnv();
+
+ jobject ttsManager = env->CallObjectMethod(_jobj, _MID_getTTSManager);
+
+ if (env->ExceptionCheck()) {
+ LOGE("Error getting TTS manager");
+
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+
+ return ttsManager;
+}
+
// The following adds assets folder to search set.
// However searching and retrieving from "assets" on Android this is slow
// so we also make sure to add the base directory, with a higher priority
@@ -739,6 +755,7 @@ void JNI::create(JNIEnv *env, jobject self, jobject asset_manager,
FIND_METHOD(, getScummVMLogPath, "()Ljava/lang/String;");
FIND_METHOD(, setCurrentGame, "(Ljava/lang/String;)V");
FIND_METHOD(, notifyHTTPService, "(IZ)V");
+ FIND_METHOD(, getTTSManager, "()Lorg/scummvm/scummvm/TextToSpeechManager;");
FIND_METHOD(, getSysArchives, "()[Ljava/lang/String;");
FIND_METHOD(, getAllStorageLocations, "()[Ljava/lang/String;");
FIND_METHOD(, initSurface, "()Ljavax/microedition/khronos/egl/EGLSurface;");
diff --git a/backends/platform/android/jni-android.h b/backends/platform/android/jni-android.h
index d8932ca806a..dc850d754ab 100644
--- a/backends/platform/android/jni-android.h
+++ b/backends/platform/android/jni-android.h
@@ -101,6 +101,7 @@ public:
static jint getAndroidSDKVersionId();
static void setCurrentGame(const Common::String &target);
static void notifyHTTPService(int localPort, bool minimal);
+ static jobject getTTSManager();
static inline bool haveSurface();
static inline bool swapBuffers();
@@ -159,6 +160,7 @@ private:
static jmethodID _MID_getScummVMLogPath;
static jmethodID _MID_setCurrentGame;
static jmethodID _MID_notifyHTTPService;
+ static jmethodID _MID_getTTSManager;
static jmethodID _MID_getSysArchives;
static jmethodID _MID_getAllStorageLocations;
static jmethodID _MID_initSurface;
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVM.java b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
index 4855cc684f7..02bff3af178 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVM.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVM.java
@@ -129,6 +129,8 @@ public abstract class ScummVM implements SurfaceHolder.Callback,
/** @noinspection unused */ @Keep
abstract protected void notifyHTTPService(int localPort, boolean minimal);
/** @noinspection unused */ @Keep
+ abstract protected TextToSpeechManager getTTSManager();
+ /** @noinspection unused */ @Keep
abstract protected String[] getSysArchives();
/** @noinspection unused */ @Keep
abstract protected String[] getAllStorageLocations();
diff --git a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
index 0129893cc06..740cb73a090 100644
--- a/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
+++ b/backends/platform/android/org/scummvm/scummvm/ScummVMActivity.java
@@ -165,6 +165,8 @@ public class ScummVMActivity extends Activity {
private static final int MY_PERMISSION_LOCAL_NETWORK = 200;
private NsdManager.RegistrationListener nsdRegistrationListener = null;
+ private TextToSpeechManager _tts = null;
+
// Set to true in onDestroy
// This avoids that when C++ terminates we call finish() a second time
// This second finish causes termination when we are launched again
@@ -966,6 +968,11 @@ public class ScummVMActivity extends Activity {
}
}
+ @Override
+ protected TextToSpeechManager getTTSManager() {
+ return _tts;
+ }
+
@Override
protected String[] getSysArchives() {
File assetsDir = new File(_actualScummVMDataDir, "assets");
@@ -1128,6 +1135,8 @@ public class ScummVMActivity extends Activity {
_clipboardManager = (android.content.ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ _tts = TextToSpeechManager.make(this);
+
// REMOVED: Since getFilesDir() is guaranteed to exist, getFilesDir().mkdirs() might be related to crashes in Android version 9+ (Pie or above, API 28+)!
// REMOVED: Setting savePath to Environment.getExternalStorageDirectory() + "/ScummVM/Saves/"
diff --git a/backends/platform/android/org/scummvm/scummvm/TextToSpeechManager.java b/backends/platform/android/org/scummvm/scummvm/TextToSpeechManager.java
new file mode 100644
index 00000000000..d84a3b14831
--- /dev/null
+++ b/backends/platform/android/org/scummvm/scummvm/TextToSpeechManager.java
@@ -0,0 +1,376 @@
+package org.scummvm.scummvm;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.speech.tts.Voice;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class TextToSpeechManager extends UtteranceProgressListener implements TextToSpeech.OnInitListener {
+ protected native void updateVoices(String[] names);
+
+ protected static class Utterance {
+ Utterance(@NonNull String utteranceId) {
+ id = utteranceId;
+ }
+
+ final String id;
+ String text;
+ float speechRate;
+ float pitch;
+ float volume;
+ int activeVoice;
+
+ boolean match(String text) {
+ return CharSequence.compare(this.text, text) == 0;
+ }
+ }
+
+ // These values are synchronized with backends/text-to-speech/android/android-text-to-speech.h
+ protected final int STATE_BROKEN = 0;
+ protected final int STATE_READY = 1;
+ protected final int STATE_SPEAKING = 2;
+ protected final int STATE_PAUSED = 3;
+
+ // These values are synchronized with common/text-to-speech.h
+ public final int ACTION_INTERRUPT = 0;
+ public final int ACTION_INTERRUPT_NO_REPEAT = 1;
+ /** @noinspection unused */
+ public final int ACTION_QUEUE = 2;
+ public final int ACTION_QUEUE_NO_REPEAT = 3;
+ public final int ACTION_DROP = 4;
+
+ private final String _obsoleteVoiceName;
+
+ protected Context _context;
+ protected TextToSpeech _tts;
+ protected Locale _locale;
+ protected final AtomicInteger _state = new AtomicInteger(STATE_BROKEN);
+ protected final LinkedList<Utterance> _queue = new LinkedList<>();
+ protected final AtomicReference<Utterance> _currentUtterance = new AtomicReference<>();
+ private final AtomicInteger _utteranceCounter = new AtomicInteger();
+
+ public static TextToSpeechManager make(Context context) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ return new TextToSpeechManager.Lollipop(context);
+ } else {
+ return new TextToSpeechManager(context);
+ }
+ }
+
+ /*
+ * We need a specific class because having
+ * a member variable of an unknown type causes errors
+ */
+ @RequiresApi(api = android.os.Build.VERSION_CODES.LOLLIPOP)
+ private static class Lollipop extends TextToSpeechManager {
+ protected Voice[] _voices = null;
+
+ public Lollipop(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void speak(Utterance utterance) {
+ if (0 <= utterance.activeVoice && utterance.activeVoice < _voices.length) {
+ _tts.setVoice(_voices[utterance.activeVoice]);
+ }
+ Bundle bundle = new Bundle();
+ bundle.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, utterance.volume);
+
+ _tts.speak(utterance.text, TextToSpeech.QUEUE_ADD, bundle, utterance.id);
+ }
+
+ @Override
+ protected void updateVoices() {
+ // Very limited voices support afterward: no gender, no age
+ // Every voice are listed independent of the chosen language, so filter them
+ Locale currentLocale = _tts.getVoice().getLocale();
+ Set<Voice> set = _tts.getVoices();
+ Iterator<Voice> it = set.iterator();
+ while(it.hasNext()) {
+ Voice v = it.next();
+ if (!v.getLocale().equals(currentLocale)) {
+ it.remove();
+ }
+ }
+ _voices = set.toArray(new Voice[]{});
+ Arrays.sort(_voices, (Voice l, Voice r) -> {
+ boolean l_bool = l.isNetworkConnectionRequired();
+ boolean r_bool = r.isNetworkConnectionRequired();
+ if (l_bool != r_bool) {
+ return l_bool ? 1 : -1;
+ }
+ int delta = l.getLatency() - r.getLatency();
+ if (delta != 0) {
+ return delta;
+ }
+ delta = l.getQuality() - r.getQuality();
+ if (delta != 0) {
+ return delta;
+ }
+ l_bool = l.getFeatures().contains("legacySetLanguageVoice");
+ r_bool = r.getFeatures().contains("legacySetLanguageVoice");
+ if (l_bool != r_bool) {
+ return l_bool ? 1 : -1;
+ }
+ return l.getName().compareTo(r.getName());
+ });
+
+ String[] voicesNames = new String[_voices.length];
+ for (int i = 0; i < _voices.length; i++) {
+ voicesNames[i] = _voices[i].getName();
+ }
+ updateVoices(voicesNames);
+ }
+ }
+
+ private TextToSpeechManager(Context context) {
+ _context = context;
+ _obsoleteVoiceName = context.getResources().getString(R.string.tts_voice_name);
+ _locale = null;
+ _tts = new TextToSpeech(context, this);
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public void shutdown() {
+ _state.set(STATE_BROKEN);
+
+ if (_tts == null) {
+ return;
+ }
+
+ _tts.shutdown();
+ synchronized (_queue) {
+ _queue.clear();
+ }
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public int getState() {
+ return _state.get();
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public void setLanguage(String language) {
+ _locale = CompatHelpers.LocaleCompat.buildLocale(language);
+ if (_state.get() == STATE_BROKEN) {
+ return;
+ }
+ _tts.setLanguage(_locale);
+ updateVoices();
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public boolean stop() {
+ if (_state.get() == STATE_BROKEN) {
+ return false;
+ }
+
+ synchronized (_queue) {
+ _queue.clear();
+ _state.set(STATE_READY);
+ }
+ _tts.stop();
+
+ return true;
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public boolean pause() {
+ if (_state.get() == STATE_BROKEN) {
+ return false;
+ }
+ if (_state.get() == STATE_PAUSED) {
+ return true;
+ }
+ if (_state.compareAndSet(STATE_SPEAKING, STATE_PAUSED)) {
+ _tts.stop();
+ return true;
+ }
+ return _state.compareAndSet(STATE_READY, STATE_PAUSED);
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public boolean resume() {
+ if (_state.get() == STATE_BROKEN) {
+ return false;
+ }
+ if (!_state.compareAndSet(STATE_PAUSED, STATE_READY)) {
+ return false;
+ }
+ startNextSpeech(true);
+ return true;
+ }
+
+ // Called by native side
+ /** @noinspection unused */ @Keep
+ public boolean say(String text, int action, int speechRate, int pitch, int volume, int activeVoice) {
+ if (_state.get() == STATE_BROKEN) {
+ return false;
+ }
+
+ synchronized (_queue) {
+ Utterance currentUtterance = _currentUtterance.get();
+ if (currentUtterance != null || !_queue.isEmpty()) {
+ if (action == ACTION_DROP) {
+ return true;
+ }
+ if (action == ACTION_INTERRUPT) {
+ _queue.clear();
+ _tts.stop();
+ } else if (action == ACTION_INTERRUPT_NO_REPEAT) {
+ _queue.clear();
+ if (currentUtterance != null && currentUtterance.match(text)) {
+ // Current text matches: stop after it
+ return true;
+ } else {
+ _tts.stop();
+ }
+ } else if (action == ACTION_QUEUE_NO_REPEAT) {
+ Utterance bottom = _queue.pollLast();
+ if (bottom == null) {
+ bottom = currentUtterance;
+ }
+ if (bottom != null && bottom.match(text)) {
+ return true;
+ }
+ }
+ }
+
+ Utterance utterance = new Utterance(nextUtteranceId());
+ utterance.text = text;
+ utterance.speechRate = speechRate / 100.f + 1.f;
+ utterance.pitch = pitch / 200.f + 1.f;
+ utterance.volume = volume / 100.f;
+ utterance.activeVoice = activeVoice;
+ _queue.add(utterance);
+ }
+
+ if (_state.get() == STATE_READY) {
+ startNextSpeech(true);
+ }
+
+ return true;
+ }
+
+ private void startNextSpeech(boolean restart) {
+ if (restart) {
+ if (!_state.compareAndSet(STATE_READY, STATE_SPEAKING)) {
+ return;
+ }
+ } else if (_state.get() != STATE_SPEAKING) {
+ return;
+ }
+
+ Utterance utterance;
+ synchronized (_queue) {
+ utterance = _queue.pollFirst();
+ }
+ if (utterance == null) {
+ // queue is empty
+ _state.compareAndSet(STATE_SPEAKING, STATE_READY);
+ return;
+ }
+
+ boolean set = _currentUtterance.compareAndSet(null, utterance);
+ if (!set) {
+ // we may have just asked for stop but we are still speaking
+ // We will get called again eventually by onDone
+ return;
+ }
+
+ _tts.setSpeechRate(utterance.speechRate);
+ _tts.setPitch(utterance.pitch);
+
+ speak(utterance);
+ }
+
+ @SuppressWarnings({"deprecation", "RedundantSuppression"})
+ protected void speak(Utterance utterance) {
+ HashMap<String, String> params = new HashMap<>();
+ params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utterance.id);
+ params.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(utterance.volume));
+ _tts.speak(utterance.text, TextToSpeech.QUEUE_ADD, params);
+ }
+
+ protected String nextUtteranceId() {
+ int newUtterance = _utteranceCounter.getAndIncrement();
+ return Integer.toHexString(newUtterance);
+ }
+
+ protected void updateVoices() {
+ // No voice support before Lollipop
+ String[] voices = new String[] { _obsoleteVoiceName };
+ updateVoices(voices);
+ }
+
+ // OnInitListener API
+ @Override
+ public void onInit(int status) {
+ if (status != TextToSpeech.SUCCESS) {
+ _tts = null;
+ return;
+ }
+
+ _tts.setOnUtteranceProgressListener(this);
+ if (_locale != null) {
+ _tts.setLanguage(_locale);
+ updateVoices();
+ }
+ _state.set(STATE_READY);
+ }
+
+ // UtteranceProgressListener API
+ @Override
+ public void onStart(String utteranceId) {
+ //Log.d(ScummVM.LOG_TAG, "TTS: onStart " + utteranceId);
+ // Nothing to do
+ Utterance utterance = _currentUtterance.get();
+ assert(utterance != null && utterance.id.equals(utteranceId));
+ }
+
+ @Override
+ public void onDone(String utteranceId) {
+ //Log.d(ScummVM.LOG_TAG, "TTS: onDone " + utteranceId);
+ Utterance utterance = _currentUtterance.get();
+ assert(utterance.id.equals(utteranceId));
+ boolean reset = _currentUtterance.compareAndSet(utterance, null);
+ assert(reset);
+ startNextSpeech(false);
+ }
+
+ @SuppressWarnings({"deprecation", "RedundantSuppression"})
+ @Override
+ public void onError(String utteranceId) {
+ //Log.d(ScummVM.LOG_TAG, "TTS: onError " + utteranceId);
+ onDone(utteranceId);
+ }
+
+ @RequiresApi(api = android.os.Build.VERSION_CODES.M)
+ @Override
+ public void onStop(String utteranceId, boolean interrupted) {
+ //Log.d(ScummVM.LOG_TAG, "TTS: onStop " + utteranceId + " " + interrupted);
+ // On Android M and above, onStop is called instead of onDone if it has been stopped
+ onDone(utteranceId);
+ }
+}
diff --git a/backends/text-to-speech/android/android-text-to-speech.cpp b/backends/text-to-speech/android/android-text-to-speech.cpp
new file mode 100644
index 00000000000..4ebfc6ec733
--- /dev/null
+++ b/backends/text-to-speech/android/android-text-to-speech.cpp
@@ -0,0 +1,288 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Allow use of stuff in <time.h> and abort()
+#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
+#define FORBIDDEN_SYMBOL_EXCEPTION_abort
+
+// Disable printf override in common/forbidden.h to avoid
+// clashes with log.h from the Android SDK.
+// That header file uses
+// __attribute__ ((format(printf, 3, 4)))
+// which gets messed up by our override mechanism; this could
+// be avoided by either changing the Android SDK to use the equally
+// legal and valid
+// __attribute__ ((format(__printf__, 3, 4)))
+// or by refining our printf override to use a varadic macro
+// (which then wouldn't be portable, though).
+// Anyway, for now we just disable the printf override globally
+// for the Android port
+#define FORBIDDEN_SYMBOL_EXCEPTION_printf
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#if defined(USE_TTS)
+#include "backends/text-to-speech/android/android-text-to-speech.h"
+
+#include "backends/platform/android/android.h"
+#include "backends/platform/android/jni-android.h"
+
+#include "common/translation.h"
+#include "common/ustr.h"
+
+jmethodID AndroidTextToSpeechManager::_MID_shutdown = 0;
+jmethodID AndroidTextToSpeechManager::_MID_getState = 0;
+jmethodID AndroidTextToSpeechManager::_MID_setLanguage = 0;
+jmethodID AndroidTextToSpeechManager::_MID_stop = 0;
+jmethodID AndroidTextToSpeechManager::_MID_pause = 0;
+jmethodID AndroidTextToSpeechManager::_MID_resume = 0;
+jmethodID AndroidTextToSpeechManager::_MID_say = 0;
+
+const JNINativeMethod AndroidTextToSpeechManager::_natives[] = {
+ { "updateVoices", "([Ljava/lang/String;)V",
+ (void *)static_cast<void (*)(JNIEnv *, jobject, jobjectArray)>(AndroidTextToSpeechManager::updateVoices) },
+};
+
+bool AndroidTextToSpeechManager::_init = false;
+
+void AndroidTextToSpeechManager::initJNI(JNIEnv *env) {
+ if (_init) {
+ return;
+ }
+
+ // We can't call error here as the backend is not built yet
+#define FIND_METHOD(prefix, name, signature) do { \
+ _MID_ ## prefix ## name = env->GetMethodID(cls, #name, signature); \
+ if (_MID_ ## prefix ## name == 0) { \
+ LOGE("Can't find method ID " #name); \
+ abort(); \
+ } \
+ } while (0)
+
+ jclass cls = env->FindClass("org/scummvm/scummvm/TextToSpeechManager");
+
+ FIND_METHOD(, shutdown, "()V");
+ FIND_METHOD(, getState, "()I");
+ FIND_METHOD(, setLanguage, "(Ljava/lang/String;)V");
+ FIND_METHOD(, stop, "()Z");
+ FIND_METHOD(, pause, "()Z");
+ FIND_METHOD(, resume, "()Z");
+ FIND_METHOD(, say, "(Ljava/lang/String;IIIII)Z");
+
+ if (env->RegisterNatives(cls, _natives, ARRAYSIZE(_natives)) < 0) {
+ LOGE("Can't register natives for org/scummvm/scummvm/TextToSpeechManager");
+ abort();
+ }
+
+ env->DeleteLocalRef(cls);
+
+#undef FIND_FIELD
+#undef FIND_METHOD
+
+ _init = true;
+}
+
+AndroidTextToSpeechManager::AndroidTextToSpeechManager() : _tts(nullptr) {
+ JNIEnv *env = JNI::getEnv();
+
+ initJNI(env);
+
+ jobject tts = JNI::getTTSManager();
+ if (tts == nullptr) {
+ return;
+ }
+ _tts = env->NewGlobalRef(tts);
+ env->DeleteLocalRef(tts);
+}
+
+AndroidTextToSpeechManager::~AndroidTextToSpeechManager() {
+ stop();
+ JNIEnv *env = JNI::getEnv();
+
+ env->CallVoidMethod(_tts, _MID_shutdown);
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::shutdown failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+
+ env->DeleteGlobalRef(_tts);
+ _tts = nullptr;
+
+ clearState();
+}
+
+void AndroidTextToSpeechManager::updateVoices(JNIEnv *env, jobject obj, jobjectArray voices) {
+ AndroidTextToSpeechManager *tts = (AndroidTextToSpeechManager *)g_system->getTextToSpeechManager();
+ assert(tts);
+
+ Common::String currentVoice;
+ if (!tts->_ttsState->_availableVoices.empty())
+ currentVoice = tts->_ttsState->_availableVoices[tts->_ttsState->_activeVoice].getDescription();
+ int activeVoiceIndex = -1;
+
+ tts->_ttsState->_availableVoices.clear();
+
+ jsize size = env->GetArrayLength(voices);
+ for (jsize i = 0; i < size; i++) {
+ jstring name_obj = (jstring)env->GetObjectArrayElement(voices, i);
+ const char *name = env->GetStringUTFChars(name_obj, 0);
+ if (name == nullptr) {
+ env->DeleteLocalRef(name_obj);
+ continue;
+ }
+
+ jsize *idx_p = new jsize;
+ *idx_p = i;
+
+ // Android doesn't provide any gender/age information on voices
+ Common::TTSVoice voice(Common::TTSVoice::UNKNOWN_GENDER, Common::TTSVoice::UNKNOWN_AGE, idx_p, name);
+ tts->_ttsState->_availableVoices.push_back(voice);
+
+ if (name == currentVoice)
+ activeVoiceIndex = i;
+
+ env->ReleaseStringUTFChars(name_obj, name);
+ env->DeleteLocalRef(name_obj);
+ }
+
+ if (activeVoiceIndex == -1 && size > 0) {
+ activeVoiceIndex = 0;
+ }
+ if (activeVoiceIndex != -1) {
+ tts->setVoice(activeVoiceIndex);
+ }
+}
+
+void AndroidTextToSpeechManager::setVoice(unsigned index) {
+ if (_ttsState->_availableVoices.empty())
+ return;
+ assert(index < _ttsState->_availableVoices.size());
+ _ttsState->_activeVoice = index;
+}
+
+int AndroidTextToSpeechManager::getState() const {
+ JNIEnv *env = JNI::getEnv();
+
+ jint ret = env->CallIntMethod(_tts, _MID_getState);
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::getState failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ return BROKEN;
+ }
+
+ return ret;
+}
+
+void AndroidTextToSpeechManager::setLanguage(Common::String language) {
+ Common::TextToSpeechManager::setLanguage(language);
+
+ JNIEnv *env = JNI::getEnv();
+
+ jstring language_obj = env->NewStringUTF(_ttsState->_language.c_str());
+
+ env->CallVoidMethod(_tts, _MID_setLanguage, language_obj);
+
+ env->DeleteLocalRef(language_obj);
+
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::setLanguage failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ }
+}
+
+bool AndroidTextToSpeechManager::stop() {
+ JNIEnv *env = JNI::getEnv();
+
+ jboolean ret = env->CallBooleanMethod(_tts, _MID_stop);
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::stop failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return false;
+ }
+
+ return ret;
+}
+
+bool AndroidTextToSpeechManager::pause() {
+ JNIEnv *env = JNI::getEnv();
+
+ jboolean ret = env->CallBooleanMethod(_tts, _MID_pause);
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::pause failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return false;
+ }
+
+ return ret;
+}
+
+bool AndroidTextToSpeechManager::resume() {
+ JNIEnv *env = JNI::getEnv();
+
+ jboolean ret = env->CallBooleanMethod(_tts, _MID_resume);
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::resume failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return false;
+ }
+
+ return ret;
+}
+
+bool AndroidTextToSpeechManager::say(const Common::U32String &str, Action action) {
+ JNIEnv *env = JNI::getEnv();
+
+ jstring str_obj = JNI::convertToJString(env, str);
+
+ jsize voice = -1;
+ if (!_ttsState->_availableVoices.empty()) {
+ jsize *voice_p = (jsize *)_ttsState->_availableVoices[_ttsState->_activeVoice].getData();
+ assert(voice_p != nullptr);
+ voice = *voice_p;
+ }
+
+ jboolean ret = env->CallBooleanMethod(_tts, _MID_say, str_obj, (jint)action,
+ _ttsState->_rate, _ttsState->_pitch,
+ _ttsState->_volume, voice);
+
+ env->DeleteLocalRef(str_obj);
+
+ if (env->ExceptionCheck()) {
+ LOGE("TextToSpeechManager::say failed");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+
+ return false;
+ }
+
+ return ret;
+}
+
+#endif
diff --git a/backends/text-to-speech/android/android-text-to-speech.h b/backends/text-to-speech/android/android-text-to-speech.h
new file mode 100644
index 00000000000..e46674cec78
--- /dev/null
+++ b/backends/text-to-speech/android/android-text-to-speech.h
@@ -0,0 +1,90 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef BACKENDS_TEXT_TO_SPEECH_ANDROID_H
+#define BACKENDS_TEXT_TO_SPEECH_ANDROID_H
+
+#include <jni.h>
+
+#include "common/scummsys.h"
+
+#if defined(USE_TTS)
+
+#include "common/text-to-speech.h"
+#include "common/str.h"
+#include "common/ustr.h"
+#include "common/list.h"
+
+
+class AndroidTextToSpeechManager final : public Common::TextToSpeechManager {
+public:
+ enum SpeechState {
+ BROKEN,
+ READY,
+ SPEAKING,
+ PAUSED,
+ };
+
+ static void initJNI(JNIEnv *env);
+
+ AndroidTextToSpeechManager();
+ ~AndroidTextToSpeechManager() override;
+
+ bool say(const Common::U32String &str, Action action) override;
+
+ bool stop() override;
+ bool pause() override;
+ bool resume() override;
+
+ bool isSpeaking() override { return getState() == SPEAKING; }
+ bool isPaused() override { return getState() == PAUSED; }
+ bool isReady() override { return getState() == READY; }
+
+ void setVoice(unsigned index) override;
+ void freeVoiceData(void *data) override { delete (int *)data; }
+
+ void setLanguage(Common::String language) override;
+
+private:
+ static void updateVoices(JNIEnv *env, jobject obj, jobjectArray voices);
+ void updateVoices() override { setLanguage(_ttsState->_language); }
+
+ int getState() const;
+
+ jobject _tts;
+
+ static jmethodID _MID_shutdown;
+ static jmethodID _MID_getState;
+ static jmethodID _MID_setLanguage;
+ static jmethodID _MID_stop;
+ static jmethodID _MID_pause;
+ static jmethodID _MID_resume;
+ static jmethodID _MID_say;
+
+ static const JNINativeMethod _natives[];
+
+ static bool _init;
+};
+
+
+#endif
+
+#endif // BACKENDS_UPDATES_ANDROID_H
diff --git a/configure b/configure
index a6ea47a31ce..eb45d0fda27 100755
--- a/configure
+++ b/configure
@@ -5464,6 +5464,9 @@ echocheck "TTS libraries"
if test "$_tts" = auto ; then
_tts=no
case $_host_os in
+ android)
+ _tts=yes
+ ;;
mingw*)
cat > $TMPC << EOF
#include <windows.h>
@@ -7436,6 +7439,10 @@ if test "$_tts" = "no"; then
echo "no"
else
case $_host_os in
+ android)
+ echo "android"
+ _tts=yes
+ ;;
linux* | freebsd* | openbsd*)
echo "speech dispatcher"
_tts=yes
diff --git a/dists/android.strings.xml.cpp b/dists/android.strings.xml.cpp
index 56288e435a0..c63ce701e35 100644
--- a/dists/android.strings.xml.cpp
+++ b/dists/android.strings.xml.cpp
@@ -49,6 +49,7 @@ static Common::U32String customkeyboardview_keycode_enter = _("Enter");
static Common::U32String customkeyboardview_popup_close = _("Close popup");
static Common::U32String local_net_permission_denied = _("Local network access permission was denied: Web server won\'t be reachable");
static Common::U32String http_service_description = _("ScummVM Web server");
+static Common::U32String tts_voice_name = _("Default voice");
static Common::U32String ini_parsing_error = _("Configuration file could not be parsed");
static Common::U32String shortcut_creator_title = _("Run a game");
static Common::U32String shortcut_creator_search_game = _("Search a game");
diff --git a/dists/android/AndroidManifest.xml b/dists/android/AndroidManifest.xml
index df24085e393..8344aff4082 100644
--- a/dists/android/AndroidManifest.xml
+++ b/dists/android/AndroidManifest.xml
@@ -46,6 +46,12 @@
android:glEsVersion="0x00020000"
android:required="true" />
+ <queries>
+ <intent>
+ <action android:name="android.intent.action.TTS_SERVICE" />
+ </intent>
+ </queries>
+
<supports-screens
android:largeScreens="true"
android:normalScreens="true"
diff --git a/dists/android/res/values/strings.xml b/dists/android/res/values/strings.xml
index ed73f84b6bc..650d86211ad 100644
--- a/dists/android/res/values/strings.xml
+++ b/dists/android/res/values/strings.xml
@@ -41,6 +41,8 @@
<string name="local_net_permission_denied">Local network access permission was denied: Web server won\'t be reachable</string>
<string name="http_service_description">ScummVM Web server</string>
+ <string name="tts_voice_name">Default voice</string>
+
<string name="ini_parsing_error">Configuration file could not be parsed</string>
<string name="shortcut_creator_title">Run a game</string>
<string name="shortcut_creator_search_game">Search a game</string>
More information about the Scummvm-git-logs
mailing list