[Scummvm-git-logs] scummvm master -> 1224510cd1277b39552190e4f28c22bc1f649caa

sev- noreply at scummvm.org
Thu Jul 10 12:09:32 UTC 2025


This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
1224510cd1 ADL: Add text-to-speech (TTS)


Commit: 1224510cd1277b39552190e4f28c22bc1f649caa
    https://github.com/scummvm/scummvm/commit/1224510cd1277b39552190e4f28c22bc1f649caa
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-07-10T14:09:29+02:00

Commit Message:
ADL: Add text-to-speech (TTS)

Changed paths:
    engines/adl/adl.cpp
    engines/adl/adl.h
    engines/adl/detection.cpp
    engines/adl/detection.h
    engines/adl/display.cpp
    engines/adl/display.h
    engines/adl/hires1.cpp
    engines/adl/metaengine.cpp


diff --git a/engines/adl/adl.cpp b/engines/adl/adl.cpp
index 7ecf997e8cd..53ca0776517 100644
--- a/engines/adl/adl.cpp
+++ b/engines/adl/adl.cpp
@@ -237,7 +237,8 @@ Common::String AdlEngine::inputString(byte prompt) const {
 
 		if (b == ('\r' | 0x80)) {
 			s += b;
-			_display->printString(Common::String(b));
+			_display->printString(Common::String(b), false);
+			_display->sayText(s, Common::TextToSpeechManager::INTERRUPT);
 			return s;
 		}
 
@@ -256,7 +257,7 @@ Common::String AdlEngine::inputString(byte prompt) const {
 		} else {
 			if (s.size() < 255) {
 				s += b;
-				_display->printString(Common::String(b));
+				_display->printString(Common::String(b), false);
 			}
 		}
 	}
@@ -279,8 +280,10 @@ byte AdlEngine::inputKey(bool showCursor) const {
 				continue;
 
 			switch (event.kbd.keycode) {
-			case Common::KEYCODE_BACKSPACE:
 			case Common::KEYCODE_RETURN:
+				stopTextToSpeech();
+				// fall through
+			case Common::KEYCODE_BACKSPACE:
 				key = convertKey(event.kbd.keycode);
 				break;
 			default:
@@ -544,6 +547,13 @@ void AdlEngine::loadDroppedItemOffsets(Common::ReadStream &stream, byte count) {
 	}
 }
 
+void AdlEngine::stopTextToSpeech() const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+		ttsMan->stop();
+	}
+}
+
 void AdlEngine::drawPic(byte pic, Common::Point pos) const {
 	if (_roomData.pictures.contains(pic)) {
 		Common::StreamPtr stream(_roomData.pictures[pic]->createReadStream());
@@ -776,6 +786,12 @@ Common::Error AdlEngine::run() {
 	init();
 	g_system->setFeatureState(OSystem::kFeatureVirtualKeyboard, true);
 
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->enable(ConfMan.getBool("tts_enabled"));
+		ttsMan->setLanguage(ConfMan.get("language"));
+	}
+
 	int saveSlot = ConfMan.getInt("save_slot");
 	if (saveSlot >= 0) {
 		if (loadGameState(saveSlot).getCode() != Common::kNoError)
diff --git a/engines/adl/adl.h b/engines/adl/adl.h
index 3c151fdeec1..334a8e99009 100644
--- a/engines/adl/adl.h
+++ b/engines/adl/adl.h
@@ -311,6 +311,8 @@ protected:
 	virtual void advanceClock() { }
 	void loadDroppedItemOffsets(Common::ReadStream &stream, byte count);
 
+	void stopTextToSpeech() const;
+
 	// Opcodes
 	typedef Common::SharedPtr<Common::Functor1<ScriptEnv &, int> > Opcode;
 
diff --git a/engines/adl/detection.cpp b/engines/adl/detection.cpp
index 29b49edc8f8..76bf6159153 100644
--- a/engines/adl/detection.cpp
+++ b/engines/adl/detection.cpp
@@ -37,8 +37,8 @@ static const DebugChannelDef debugFlagList[] = {
 	DEBUG_CHANNEL_END
 };
 
-#define DEFAULT_OPTIONS GUIO6(GAMEOPTION_NTSC, GAMEOPTION_COLOR_DEFAULT_ON, GAMEOPTION_MONO_TEXT, GAMEOPTION_SCANLINES, GAMEOPTION_APPLE2E_CURSOR, GUIO_NOMIDI)
-#define MH_OPTIONS GUIO6(GAMEOPTION_NTSC, GAMEOPTION_COLOR_DEFAULT_OFF, GAMEOPTION_MONO_TEXT, GAMEOPTION_SCANLINES, GAMEOPTION_APPLE2E_CURSOR, GUIO_NOMIDI)
+#define DEFAULT_OPTIONS GUIO7(GAMEOPTION_NTSC, GAMEOPTION_COLOR_DEFAULT_ON, GAMEOPTION_MONO_TEXT, GAMEOPTION_SCANLINES, GAMEOPTION_APPLE2E_CURSOR, GUIO_NOMIDI, GAMEOPTION_TTS)
+#define MH_OPTIONS GUIO7(GAMEOPTION_NTSC, GAMEOPTION_COLOR_DEFAULT_OFF, GAMEOPTION_MONO_TEXT, GAMEOPTION_SCANLINES, GAMEOPTION_APPLE2E_CURSOR, GUIO_NOMIDI, GAMEOPTION_TTS)
 
 static const PlainGameDescriptor adlGames[] = {
 	{ "hires0", "Hi-Res Adventure #0: Mission Asteroid" },
diff --git a/engines/adl/detection.h b/engines/adl/detection.h
index 0026d3b23a1..faa1a698428 100644
--- a/engines/adl/detection.h
+++ b/engines/adl/detection.h
@@ -84,6 +84,7 @@ struct AdlGameDescription {
 #define GAMEOPTION_NTSC              GUIO_GAMEOPTIONS4
 #define GAMEOPTION_MONO_TEXT         GUIO_GAMEOPTIONS5
 #define GAMEOPTION_APPLE2E_CURSOR    GUIO_GAMEOPTIONS6
+#define GAMEOPTION_TTS               GUIO_GAMEOPTIONS7
 
 } // End of namespace Adl
 
diff --git a/engines/adl/display.cpp b/engines/adl/display.cpp
index ef59f4a4086..4f554981db8 100644
--- a/engines/adl/display.cpp
+++ b/engines/adl/display.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/debug.h"
 #include "common/rect.h"
 #include "common/str.h"
@@ -73,20 +74,55 @@ void Display::moveCursorTo(const Common::Point &pos) {
 		error("Cursor position (%i, %i) out of bounds", pos.x, pos.y);
 }
 
-void Display::printString(const Common::String &str) {
+void Display::printString(const Common::String &str, bool voiceString) {
 	for (const auto &c : str)
 		printChar(c);
 
+	if (voiceString) {
+		sayText(str);
+	}
+
 	renderText();
 }
 
-void Display::printAsciiString(const Common::String &str) {
+void Display::printAsciiString(const Common::String &str, bool voiceString) {
 	for (const auto &c : str)
 		printChar(asciiToNative(c));
 
+	if (voiceString) {
+		sayText(str);
+	}
+
 	renderText();
 }
 
+void Display::sayText(const Common::String &text, Common::TextToSpeechManager::Action action) const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled")) {
+		ttsMan->say(convertText(text), action);
+	}
+}
+
+Common::U32String Display::convertText(const Common::String &text) const {
+	Common::String result(text);
+
+	bool startsWithDash = result[0] == '\xad';
+
+	for (uint i = 0; i < result.size(); i++) {
+		// Convert carriage returns and dashes to spaces
+		// Only convert dashes if the line starts with a dash (which is usually the case with the "enter command"
+		// prompt), to avoid converting mid-line dashes to spaces and causing odd voicing
+		// (i.e. if the dash in "Hi-Res Adventure" is replaced, English TTS pronounces it as "Hi Residential Adventure")
+		if (result[i] == '\x8d' || (startsWithDash && result[i] == '\xad')) {
+			result[i] = '\xa0';
+		}
+
+		result[i] &= 0x7f;
+	}
+
+	return Common::U32String(result, Common::CodePage::kASCII);
+}
+
 void Display::setCharAtCursor(byte c) {
 	_textBuf[_cursorPos] = c;
 }
diff --git a/engines/adl/display.h b/engines/adl/display.h
index 9ba6680e297..ba5c68edc36 100644
--- a/engines/adl/display.h
+++ b/engines/adl/display.h
@@ -22,6 +22,7 @@
 #ifndef ADL_DISPLAY_H
 #define ADL_DISPLAY_H
 
+#include "common/text-to-speech.h"
 #include "common/types.h"
 
 namespace Common {
@@ -53,8 +54,10 @@ public:
 	void moveCursorTo(const Common::Point &pos);
 	void moveCursorForward();
 	void moveCursorBackward();
-	void printString(const Common::String &str);
-	void printAsciiString(const Common::String &str);
+	void printString(const Common::String &str, bool voiceString = true);
+	void printAsciiString(const Common::String &str, bool voiceString = true);
+	void sayText(const Common::String &text, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::QUEUE) const;
+	Common::U32String convertText(const Common::String &text) const;
 	void setCharAtCursor(byte c);
 	uint getTextWidth() const { return _textWidth; }
 	uint getTextHeight() const { return _textHeight; }
diff --git a/engines/adl/hires1.cpp b/engines/adl/hires1.cpp
index e1f40768185..b1109d5d6db 100644
--- a/engines/adl/hires1.cpp
+++ b/engines/adl/hires1.cpp
@@ -109,6 +109,8 @@ protected:
 void HiRes1Engine::showInstructions(Common::SeekableReadStream &stream) {
 	_display->setMode(Display::kModeText);
 
+	Common::String ttsMessage;
+
 	for (;;) {
 		byte opc = stream.readByte();
 
@@ -121,6 +123,9 @@ void HiRes1Engine::showInstructions(Common::SeekableReadStream &stream) {
 			// HOME
 			_display->home();
 		} else if (addr == 0x6ffd) {
+			_display->sayText(ttsMessage);
+			ttsMessage.clear();
+
 			// GETLN1
 			inputString();
 
@@ -129,6 +134,13 @@ void HiRes1Engine::showInstructions(Common::SeekableReadStream &stream) {
 		} else {
 			// We assume print string call (addr varies per game)
 			Common::String str = readString(stream);
+			ttsMessage += str;
+
+			// If the string ends in two carriage returns, add a newline to the end of the TTS message
+			// (since carriage returns will be replaced with spaces)
+			if (str.size() > 1 && str[str.size() - 1] == '\x8d' && str[str.size() - 2] == '\x8d') {
+				ttsMessage += '\n';
+			}
 
 			if (stream.err() || stream.eos())
 				error("Error reading instructions");
@@ -137,11 +149,11 @@ void HiRes1Engine::showInstructions(Common::SeekableReadStream &stream) {
 			size_t posChr4 = str.findFirstOf(_display->asciiToNative(4));
 
 			if (posChr4 != str.npos) {
-				_display->printString(str.substr(0, posChr4));
+				_display->printString(str.substr(0, posChr4), false);
 				return;
 			}
 
-			_display->printString(str);
+			_display->printString(str, false);
 		}
 	}
 }
@@ -186,18 +198,23 @@ void HiRes1Engine::runIntro() {
 		_display->home();
 
 		str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_0, '"');
-		_display->printAsciiString(str + '\r');
+		_display->printAsciiString(str + '\r', false);
+		Common::String ttsMessage = str + ' ';
 
 		str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_1, '"');
-		_display->printAsciiString(str + "\r\r");
+		_display->printAsciiString(str + "\r\r", false);
+		ttsMessage += str + '\n';
 
 		str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_2, '"');
-		_display->printAsciiString(str + "\r\r");
+		_display->printAsciiString(str + "\r\r", false);
+		ttsMessage += str + '\n';
 
 		str = readStringAt(*basic, IDI_HR1_OFS_PD_TEXT_3, '"');
-		_display->printAsciiString(str + '\r');
+		_display->printAsciiString(str + '\r', false);
+		_display->sayText(ttsMessage + ' ' + str);
 
 		inputKey();
+		stopTextToSpeech();
 		if (shouldQuit())
 			return;
 	}
@@ -636,9 +653,11 @@ void HiRes1Engine_VF::runIntro() {
 
 		if (key == _display->asciiToNative('M')) {
 			stream->seek(0x75);
+			_display->sayText("M", Common::TextToSpeechManager::INTERRUPT);
 			showInstructions(*stream);
 			return;
 		} else if (key == _display->asciiToNative('J')) {
+			_display->sayText("J", Common::TextToSpeechManager::INTERRUPT);
 			return;
 		}
 	}
diff --git a/engines/adl/metaengine.cpp b/engines/adl/metaengine.cpp
index d8723278412..0019fb68ced 100644
--- a/engines/adl/metaengine.cpp
+++ b/engines/adl/metaengine.cpp
@@ -110,6 +110,20 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		}
 	},
 
+#ifdef USE_TTS
+	{
+		GAMEOPTION_TTS,
+		{
+			_s("Enable Text to Speech"),
+			_s("Use TTS to read text in the game (if TTS is available)"),
+			"tts_enabled",
+			false,
+			0,
+			0
+		}
+	},
+#endif
+
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 




More information about the Scummvm-git-logs mailing list