[Scummvm-git-logs] scummvm master -> 8b8d72347d0a524e4721393b3fbd98461b4ba10f

sev- noreply at scummvm.org
Mon Jul 21 10:57:52 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:
8b8d72347d PARALLACTION: Add text-to-speech (TTS)


Commit: 8b8d72347d0a524e4721393b3fbd98461b4ba10f
    https://github.com/scummvm/scummvm/commit/8b8d72347d0a524e4721393b3fbd98461b4ba10f
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-07-21T12:57:49+02:00

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

Changed paths:
    engines/parallaction/balloons.cpp
    engines/parallaction/callables_ns.cpp
    engines/parallaction/detection.cpp
    engines/parallaction/detection.h
    engines/parallaction/dialogue.cpp
    engines/parallaction/exec.h
    engines/parallaction/exec_ns.cpp
    engines/parallaction/graphics.cpp
    engines/parallaction/graphics.h
    engines/parallaction/gui_ns.cpp
    engines/parallaction/metaengine.cpp
    engines/parallaction/parallaction.cpp
    engines/parallaction/parallaction.h
    engines/parallaction/parallaction_ns.cpp


diff --git a/engines/parallaction/balloons.cpp b/engines/parallaction/balloons.cpp
index 16c5f71ffb5..305c458dc3a 100644
--- a/engines/parallaction/balloons.cpp
+++ b/engines/parallaction/balloons.cpp
@@ -334,6 +334,8 @@ int BalloonManager_ns::setSingleBalloon(const Common::String &text, uint16 x, ui
 	w = _se.width() + 14;
 	h = _se.height() + 20;
 
+	_vm->sayText(text, Common::TextToSpeechManager::INTERRUPT);
+
 	int id = createBalloon(w+5, h, winding, 1);
 	Balloon *balloon = &_intBalloons[id];
 
@@ -380,6 +382,10 @@ void BalloonManager_ns::setBalloonText(uint id, const Common::String &text, Text
 	Balloon *balloon = getBalloon(id);
 	balloon->surface->fillRect(balloon->innerBox, 1);
 
+	if (textColor != kUnselectedColor && !text.contains("%P")) {
+		_vm->sayText(text, Common::TextToSpeechManager::INTERRUPT);
+	}
+
 	_sw.write(text, MAX_BALLOON_WIDTH, _textColors[textColor], balloon->surface);
 }
 
@@ -426,6 +432,10 @@ void BalloonManager_ns::reset() {
 		_intBalloons[i].surface = nullptr;	// no need to delete surface, since it is done by Gfx
 	}
 	_numBalloons = 0;
+
+	if (_vm->_password.size() == 0) {
+		_vm->stopTextToSpeech();
+	}
 }
 
 
@@ -585,6 +595,8 @@ Graphics::Surface *BalloonManager_br::expandBalloon(Frames *data, int frameNum)
 int BalloonManager_br::setSingleBalloon(const Common::String &text, uint16 x, uint16 y, uint16 winding, TextColor textColor) {
 	cacheAnims();
 
+	_vm->sayText(text, Common::TextToSpeechManager::INTERRUPT);
+
 	int id = _numBalloons;
 	Frames *src = nullptr;
 	int srcFrame = 0;
@@ -657,6 +669,11 @@ int BalloonManager_br::setDialogueBalloon(const Common::String &text, uint16 win
 
 void BalloonManager_br::setBalloonText(uint id, const Common::String &text, TextColor textColor) {
 	Balloon *balloon = getBalloon(id);
+
+	if (textColor != kUnselectedColor) {
+		_vm->sayText(text, Common::TextToSpeechManager::INTERRUPT);
+	}
+
 	_sw.write(text, 216, _textColors[textColor], balloon->surface);
 }
 
@@ -713,6 +730,8 @@ void BalloonManager_br::reset() {
 	}
 
 	_numBalloons = 0;
+
+	_vm->stopTextToSpeech();
 }
 
 void BalloonManager_br::cacheAnims() {
diff --git a/engines/parallaction/callables_ns.cpp b/engines/parallaction/callables_ns.cpp
index effa08616f5..025e1698d70 100644
--- a/engines/parallaction/callables_ns.cpp
+++ b/engines/parallaction/callables_ns.cpp
@@ -413,6 +413,23 @@ void Parallaction_ns::_c_testResult(void *parm) {
 	_gfx->showLabel(_testResultLabels[0], CENTER_LABEL_HORIZONTAL, 38);
 	_gfx->showLabel(_testResultLabels[1], CENTER_LABEL_HORIZONTAL, 58);
 
+	const char *charName = nullptr;
+	switch (_characterVoiceID) {
+	case kDoug:
+		charName = "Doug Nuts";
+		break;
+	case kDonna:
+		charName = "Donna Fatale";
+		break;
+	case kDino:
+		charName = "Dino Fagioli";
+		break;
+	}
+	
+	if (charName != nullptr) {
+		sayText(charName, Common::TextToSpeechManager::QUEUE);
+	}
+
 	return;
 }
 
diff --git a/engines/parallaction/detection.cpp b/engines/parallaction/detection.cpp
index 414db021127..2ba8b71f523 100644
--- a/engines/parallaction/detection.cpp
+++ b/engines/parallaction/detection.cpp
@@ -70,7 +70,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformDOS,
 			ADGF_NO_FLAGS,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_Nippon,
 		GF_LANG_EN | GF_LANG_FR | GF_LANG_DE | GF_LANG_IT | GF_LANG_MULT,
@@ -96,7 +96,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformDOS,
 			ADGF_NO_FLAGS,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_Nippon,
 		GF_LANG_EN | GF_LANG_FR | GF_LANG_DE | GF_LANG_IT | GF_LANG_MULT,
@@ -120,7 +120,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformAmiga,
 			ADGF_NO_FLAGS,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_Nippon,
 		GF_LANG_EN | GF_LANG_FR | GF_LANG_DE | GF_LANG_MULT,
@@ -135,7 +135,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformAmiga,
 			ADGF_DEMO,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_Nippon,
 		GF_LANG_EN | GF_DEMO,
@@ -158,7 +158,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::IT_ITA,
 			Common::kPlatformAmiga,
 			ADGF_NO_FLAGS,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_Nippon,
 		GF_LANG_IT,
@@ -173,7 +173,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_BRA,
 		GF_LANG_EN | GF_LANG_FR | GF_LANG_DE | GF_LANG_IT | GF_LANG_MULT,
@@ -187,7 +187,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformDOS,
 			ADGF_DEMO | ADGF_UNSTABLE,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_BRA,
 		GF_LANG_EN | GF_DEMO,
@@ -201,7 +201,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformAmiga,
 			ADGF_UNSTABLE,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_BRA,
 		GF_LANG_EN | GF_LANG_FR | GF_LANG_DE | GF_LANG_MULT,
@@ -215,7 +215,7 @@ static const PARALLACTIONGameDescription gameDescriptions[] = {
 			Common::UNK_LANG,
 			Common::kPlatformAmiga,
 			ADGF_DEMO | ADGF_UNSTABLE,
-			GUIO1(GUIO_NOSPEECH)
+			GUIO2(GUIO_NOSPEECH, GAMEOPTION_TTS)
 		},
 		GType_BRA,
 		GF_LANG_EN | GF_DEMO,
diff --git a/engines/parallaction/detection.h b/engines/parallaction/detection.h
index 5b0d88b8eae..25d3b3eba13 100644
--- a/engines/parallaction/detection.h
+++ b/engines/parallaction/detection.h
@@ -49,6 +49,8 @@ struct PARALLACTIONGameDescription {
 	uint32 features;
 };
 
+#define GAMEOPTION_TTS                    GUIO_GAMEOPTIONS1
+
 } // End of namespace Parallaction
 
 #endif // PARALLACTION_DETECTION_H
diff --git a/engines/parallaction/dialogue.cpp b/engines/parallaction/dialogue.cpp
index 76367b42df5..53bdf03481c 100644
--- a/engines/parallaction/dialogue.cpp
+++ b/engines/parallaction/dialogue.cpp
@@ -49,6 +49,7 @@ struct BalloonPositions {
 
 
 
+static const int kNumberOfVoiceDatas = 65;
 
 class DialogueManager {
 
@@ -60,6 +61,9 @@ class DialogueManager {
 	GfxObj			*_answerer;
 	int				_faceId;
 
+	int 			_questionerVoiceID;
+	int 			_answererVoiceID;
+
 	Question		*_q;
 
 	int			_answerId;
@@ -137,6 +141,23 @@ DialogueManager::DialogueManager(Parallaction *vm, ZonePtr z) : _vm(vm), _z(z) {
 	_questioner = isNpc ? _vm->_disk->loadTalk(_z->u._filename.c_str()) : _vm->_char._talk;
 	_answerer = _vm->_char._talk;
 
+#ifdef USE_TTS
+	_answererVoiceID = _vm->_characterVoiceID;
+
+	if (!isNpc) {
+		_questionerVoiceID = _answererVoiceID;
+	} else {
+		_questionerVoiceID = kNarratorVoiceID;
+
+		for (int i = 1; i < kNumberOfVoiceDatas; ++i) {
+			if (characterVoiceDatas[i].characterName && !scumm_stricmp(characterVoiceDatas[i].characterName, _z->u._filename.c_str())) {
+				_questionerVoiceID = i;
+				break;
+			}
+		}
+	}
+#endif
+
 	_cmdList = nullptr;
 	_answerId = 0;
 
@@ -213,6 +234,8 @@ bool DialogueManager::testAnswerFlags(Answer *a) {
 
 void DialogueManager::displayAnswers() {
 
+	_vm->setTTSVoice(_answererVoiceID);
+
 	// create balloons
 	int id;
 	for (int i = 0; i < _numVisAnswers; ++i) {
@@ -278,6 +301,15 @@ int16 DialogueManager::selectAnswerN() {
 bool DialogueManager::displayQuestion() {
 	if (_q->textIsNull()) return false;
 
+#ifdef USE_TTS
+	// Some dialogue exchanges involve more than 1 character, differentiated by the mood
+	if (_questionerVoiceID < kNumberOfVoiceDatas - 1 && characterVoiceDatas[_questionerVoiceID + 1].characterName == nullptr) {
+		_vm->setTTSVoice(_questionerVoiceID + _q->speakerMood());
+	} else {
+		_vm->setTTSVoice(_questionerVoiceID);
+	}
+#endif
+
 	_balloonMan->setSingleBalloon(_q->_text, _ballonPos._questionBalloon.x, _ballonPos._questionBalloon.y, _q->balloonWinding(), BalloonManager::kNormalColor);
 	_faceId = _gfx->setItem(_questioner, _ballonPos._questionChar.x, _ballonPos._questionChar.y);
 	_gfx->setItemFrame(_faceId, _q->speakerMood());
@@ -415,6 +447,7 @@ protected:
 		}
 
 		if ((_vm->_password.size() == MAX_PASSWORD_LENGTH) || ((_isKeyDown) && (_downKey == Common::KEYCODE_RETURN))) {
+			_vm->sayText(_vm->_password, Common::TextToSpeechManager::INTERRUPT);
 			if (checkPassword()) {
 				return 0;
 			} else {
@@ -448,6 +481,8 @@ public:
 
 			if (a->_text.contains("%P")) {
 				_askPassword = true;
+
+				_vm->sayText(a->_text.substr(0, a->_text.find('@')), Common::TextToSpeechManager::INTERRUPT);
 			}
 
 			_visAnswers[_numVisAnswers]._a = a;
@@ -535,6 +570,8 @@ void Parallaction::exitDialogueMode() {
 	debugC(1, kDebugDialogue, "Parallaction::exitDialogueMode()");
 	_input->_inputMode = Input::kInputModeGame;
 
+	setTTSVoice(_characterVoiceID);
+
 	/* Since the current instance of _dialogueMan must be destroyed before the
 	   zone commands are executed, as they may create a new instance of _dialogueMan that
 	   would overwrite the current, we need to save the references to the command lists.
diff --git a/engines/parallaction/exec.h b/engines/parallaction/exec.h
index 667f735a919..36f2dc71ee7 100644
--- a/engines/parallaction/exec.h
+++ b/engines/parallaction/exec.h
@@ -202,6 +202,8 @@ public:
 
 class ProgramExec_ns : public ProgramExec {
 protected:
+	int _currentCredit;
+
 	Parallaction_ns *_vm;
 
 	DECLARE_UNQUALIFIED_INSTRUCTION_OPCODE(invalid);
diff --git a/engines/parallaction/exec_ns.cpp b/engines/parallaction/exec_ns.cpp
index e5f143ab48b..d93825d78dd 100644
--- a/engines/parallaction/exec_ns.cpp
+++ b/engines/parallaction/exec_ns.cpp
@@ -24,10 +24,201 @@
 #include "parallaction/parallaction.h"
 #include "parallaction/sound.h"
 
+#include "common/config-manager.h"
 #include "common/textconsole.h"
 
 namespace Parallaction {
 
+#ifdef USE_TTS
+
+// Transcribed for TTS; only English is translated in-game
+static const char *openingCreditsSecondLine[] = {
+	"Grafica Totale: Max M.",		// Italian
+	"Graphiques Totaux: Max M.",	// French
+	"Total Graphics: Max M.",		// English
+	"Gesamtgrafik: Max M."			// German
+};
+
+static const char *openingCreditsThirdLine[] = {
+	"Design del Gioco: Mr. Tzutzumi",	// Italian
+	"Conception de Jeux: Mr. Tzutzumi",	// French
+	"Game Design: Mr. Tzutzumi",		// English
+	"Spieldesign: Mr. Tzutzumi"			// German
+};
+
+// Transcribed for TTS; only Italian is translated in-game
+static const char *endCreditsItalian[] = {
+	"La Cassiera",
+	"La Segretaria",
+	"Il Taxista",
+	"Il Giornalaio",
+	"Passante",
+	"Il Segretario",
+	"L'Uccello",
+	"L'Inserviente",
+	"Il Priore",
+	"Il Suicida",
+	"Il Direttore",
+	"Passante",
+	"Fratello Shinpui",
+	"Apollo il Gorilla",
+	"Il Portiere",
+	"Il Guru",
+	"Fratello Baka",
+	"Josko il Barman",
+	"Figaro L' Estetista",
+	"Il Guardiano del Museo",
+	"Passante",
+	"Il Losco Max",
+	"Mister Y",
+	"Il Punk",
+	"Passante",
+	"Il Signor Bemutsu",
+	"L' Annunciatore",
+	"La Punkina",
+	"L' Imperatore",
+	"Il Dottor Ki",
+	"Il Cuoco",
+	"Il Punk",
+	"Passante",
+	"Il Losco Figuro",
+	"Chan L'Onesto",
+	"",
+	"La Geisha",
+	"Kos 'O Professore",
+	"Il Giocatore di Pacinko"
+};
+
+static const char *endCreditsFrench[] = {
+	"La Cassi\350re",
+	"La Secr\351taire",
+	"Le Chaffeur de Taxi",
+	"Le Marchand de Journaux",
+	"La Pi\351tonne",
+	"Le Secr\351taire",
+	"La Corneille",
+	"Le Oshiya",
+	"Le Prieur",
+	"Le Candidat au Suicide",
+	"Le Directeur",
+	"La Pi\351tonne",
+	"Fr\350re Shinpui",
+	"Apollo le Garde du Corps",
+	"Le Concierge",
+	"Le Guru",
+	"Fr\350re Baka",
+	"Josko le Barman",
+	"Figaro L'esth\351ticien",
+	"Le Gardien du Mus\351e",
+	"La Pi\351tonne",
+	"Le Ombrag\351e Max",
+	"Monsieur Y",
+	"Le Punk",
+	"La Pi\351tonne",
+	"Monsieur Bemutsu",
+	"Le Annonceur",
+	"Le Punk",
+	"L'Empereur",
+	"Le Docteur Ki",
+	"Le Chef",
+	"Le Punk",
+	"Le Pi\351ton",
+	"Le Louche Personnage",
+	"Honest Chan",
+	"",
+	"La Geisha",
+	"Professeur Kos",
+	"Le Joueur de Pachinko"
+};
+
+static const char *endCreditsEnglish[] = {
+	"Cashier",
+	"Secretary",
+	"Taxi-driver",
+	"Newspaper Seller",
+	"Pedestrian",
+	"Secretary",
+	"Grackle",
+	"Oshiya",
+	"Prior",
+	"Suicidal Man",
+	"Governor",
+	"Pedestrian",
+	"Brother Shinpui",
+	"Apollo the Bodyguard",
+	"Door-keeper",
+	"Guru",
+	"Brother Baka",
+	"Josko the Barman",
+	"Figaro the Beautician",
+	"Museum Custodian",
+	"Pedestrian",
+	"Sullen Max",
+	"Mister Y",
+	"Punk",
+	"Pedestrian",
+	"Mister Bemutsu",
+	"Announcer",
+	"Punk",
+	"Emperor",
+	"Doctor Ki",
+	"Cook",
+	"Punk",
+	"Pedestrian",
+	"Shady Type",
+	"Honest Chan",
+	"",
+	"Geisha",
+	"Professor Kos",
+	"Pachinko Player"
+};
+
+static const char *endCreditsGerman[] = {
+	"Die Kassiererin",
+	"Die Sekret\344rin",
+	"Der Taxifahrer",
+	"Der Zeitungsverk\344ufer",
+	"Die Passantin",
+	"Der Sekret\344r",
+	"Des Grakula",
+	"Der Oshiya",
+	"Der Prior",
+	"Der Selbstm\366rder",
+	"Der Direktor",
+	"Die Passantin",
+	"Bruder Shinpui",
+	"Apollo der Bodyguard",
+	"Der Portier",
+	"Der Guru",
+	"Bruder Baka",
+	"Josko der Barman",
+	"Figaro, der Kosmetiker",
+	"Der Museumsw\344rter",
+	"Die Passantin",
+	"Schattig Max",
+	"Herr Y",
+	"Der Punker",
+	"Die Passantin",
+	"Herr Bemutsu",
+	"Der Ansager",
+	"Der Punker",
+	"Der Kaiser",
+	"Doktor Ki",
+	"Der Koch",
+	"Der Punker",
+	"Der Passant",
+	"Der Dunkler Typ",
+	"Honest Chan",
+	"",
+	"Die Geisha",
+	"Professor Kos",
+	"Der Pachinko-Spieler"
+};
+
+static const int kNumberOfCredits = ARRAYSIZE(endCreditsItalian);
+
+#endif
+
 #define INST_ON							1
 #define INST_OFF						2
 #define INST_X							3
@@ -68,6 +259,16 @@ DECLARE_INSTRUCTION_OPCODE(on) {
 
 	inst->_a->_flags |= kFlagsActive;
 	inst->_a->_flags &= ~kFlagsRemove;
+
+#ifdef USE_TTS
+	if (scumm_stricmp(inst->_a->_name, "telo3") == 0) {
+		_vm->setTTSVoice(kNarratorVoiceID);
+		_vm->sayText(openingCreditsSecondLine[_vm->getInternLanguage()], Common::TextToSpeechManager::INTERRUPT);
+	} else if (scumm_stricmp(inst->_a->_name, "game") == 0) {
+		_vm->setTTSVoice(kNarratorVoiceID);
+		_vm->sayText(openingCreditsThirdLine[_vm->getInternLanguage()], Common::TextToSpeechManager::INTERRUPT);
+	}
+#endif
 }
 
 
@@ -85,6 +286,21 @@ DECLARE_INSTRUCTION_OPCODE(loop) {
 
 
 DECLARE_INSTRUCTION_OPCODE(endloop) {
+	if (ctxt._program->_loopStart == 11 && scumm_stricmp(_vm->_location._name, "test") == 0) {
+		// Delay moving on from test results until TTS is done or the player clicks, 
+		// so the TTS system can speak them fully
+		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+		if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+			ctxt._program->_loopCounter = 2;
+			int event = _vm->_input->getLastButtonEvent();
+
+			if (event == kMouseLeftUp) {
+				ttsMan->stop();
+				ctxt._program->_loopCounter = 0;
+			}
+		}
+	}
+
 	if (--ctxt._program->_loopCounter > 0) {
 		ctxt._ip = ctxt._program->_loopStart;
 	}
@@ -104,6 +320,33 @@ DECLARE_INSTRUCTION_OPCODE(inc) {
 	int16 lvalue = inst->_opA.getValue();
 
 	if (inst->_index == INST_INC) {
+#ifdef USE_TTS
+		if (_vm->_endCredits && ctxt._anim->_name[0] == 's' && _currentCredit < kNumberOfCredits) {
+			const char **credits;
+
+			switch (_vm->getInternLanguage()) {
+			case kItalian:
+				credits = endCreditsItalian;
+				break;
+			case kFrench:
+				credits = endCreditsFrench;
+				break;
+			case kEnglish:
+				credits = endCreditsEnglish;
+				break;
+			case kGerman:
+				credits = endCreditsGerman;
+				break;
+			default:
+				credits = endCreditsItalian;
+				break;
+			}
+
+			_vm->sayText(credits[_currentCredit], Common::TextToSpeechManager::QUEUE);
+			_currentCredit++;
+		}
+#endif
+
 		lvalue += _si;
 	} else {
 		lvalue -= _si;
@@ -314,6 +557,8 @@ CommandExec_ns::CommandExec_ns(Parallaction_ns* vm) : CommandExec(vm), _vm(vm) {
 }
 
 ProgramExec_ns::ProgramExec_ns(Parallaction_ns *vm) : _vm(vm) {
+	_currentCredit = 0;
+
 	_instructionNames = _instructionNamesRes_ns;
 
 	ProgramOpcodeSet *table = nullptr;
diff --git a/engines/parallaction/graphics.cpp b/engines/parallaction/graphics.cpp
index 78013419d9a..a9b146f581e 100644
--- a/engines/parallaction/graphics.cpp
+++ b/engines/parallaction/graphics.cpp
@@ -551,6 +551,7 @@ GfxObj *Gfx::renderFloatingLabel(Font *font, char *text) {
 	GfxObj *obj = new GfxObj(kGfxObjTypeLabel, new SurfaceToFrames(cnv), "floatingLabel");
 	obj->transparentKey = LABEL_TRANSPARENT_COLOR;
 	obj->layer = LAYER_FOREGROUND;
+	obj->_text = text;
 
 	return obj;
 }
@@ -559,6 +560,8 @@ void Gfx::showFloatingLabel(GfxObj *label) {
 	hideFloatingLabel();
 
 	if (label) {
+		_vm->sayText(label->_text, Common::TextToSpeechManager::INTERRUPT);
+
 		label->x = -1000;
 		label->y = -1000;
 		label->setFlags(kGfxObjVisible);
@@ -648,11 +651,13 @@ GfxObj *Gfx::createLabel(Font *font, const char *text, byte color) {
 	GfxObj *obj = new GfxObj(kGfxObjTypeLabel, new SurfaceToFrames(cnv), "label");
 	obj->transparentKey = LABEL_TRANSPARENT_COLOR;
 	obj->layer = LAYER_FOREGROUND;
+	obj->_text = text;
+	obj->_text.replace('@', ' ');
 
 	return obj;
 }
 
-void Gfx::showLabel(GfxObj *label, int16 x, int16 y) {
+void Gfx::showLabel(GfxObj *label, int16 x, int16 y, bool queueTTS, bool voiceText) {
 	if (!label) {
 		return;
 	}
@@ -673,6 +678,20 @@ void Gfx::showLabel(GfxObj *label, int16 x, int16 y) {
 	label->x = x;
 	label->y = y;
 
+	if (voiceText) {
+		_vm->setTTSVoice(kNarratorVoiceID);
+
+		Common::TextToSpeechManager::Action action;
+
+		if (queueTTS) {
+			action = Common::TextToSpeechManager::QUEUE;
+		} else {
+			action = Common::TextToSpeechManager::INTERRUPT;
+		}
+
+		_vm->sayText(label->_text, action);
+	}
+
 	_labels.push_back(label);
 }
 
diff --git a/engines/parallaction/graphics.h b/engines/parallaction/graphics.h
index 0d639a1bb7a..35729e88039 100644
--- a/engines/parallaction/graphics.h
+++ b/engines/parallaction/graphics.h
@@ -320,6 +320,8 @@ public:
 	int _pathId;
 	bool _hasPath;
 
+	Common::String _text;
+
 
 	GfxObj(uint type, Frames *frames, const char *name = NULL);
 	virtual ~GfxObj();
@@ -457,7 +459,7 @@ public:
 
 	GfxObj *renderFloatingLabel(Font *font, char *text);
 	GfxObj *createLabel(Font *font, const char *text, byte color);
-	void showLabel(GfxObj *label, int16 x, int16 y);
+	void showLabel(GfxObj *label, int16 x, int16 y, bool queueTTS = true, bool voiceText = true);
 	void hideLabel(GfxObj *label);
 	void freeLabels();
 	void unregisterLabel(GfxObj *label);
diff --git a/engines/parallaction/gui_ns.cpp b/engines/parallaction/gui_ns.cpp
index 3bd7ff0e673..d33ee56ef12 100644
--- a/engines/parallaction/gui_ns.cpp
+++ b/engines/parallaction/gui_ns.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/system.h"
 #include "common/hashmap.h"
 #include "common/textconsole.h"
@@ -32,6 +33,24 @@
 
 namespace Parallaction {
 
+#ifdef USE_TTS
+
+static const char *languageSelectBlockTexts[] = {
+	"Dizionario\nGiapponese\nItaliano",
+	"Dictionnaire\nJaponais\nFran\347ais",
+	"Dictionary\nJapanese\nEnglish",
+	"W\366rterbuch\nJapanese\nDeutsch"
+};
+
+static const char *ttsLanguages[] = {
+	"it",
+	"fr",
+	"en",
+	"de"
+};
+
+#endif
+
 class SplashInputState_NS : public MenuInputState {
 protected:
 	Common::String _slideName;
@@ -109,6 +128,7 @@ class ChooseLanguageInputState_NS : public MenuInputState {
 	Common::Rect _dosLanguageSelectBlocks[4];
 	Common::Rect _amigaLanguageSelectBlocks[4];
 	const Common::Rect *_blocks;
+	int _previousBlock;
 
 	Parallaction *_vm;
 
@@ -129,6 +149,8 @@ public:
 		_amigaLanguageSelectBlocks[2] = Common::Rect( 178,  60, 226, 130 );	// English
 		_amigaLanguageSelectBlocks[3] = Common::Rect( 227,  35, 275, 105 );	// German
 
+		_previousBlock = -1;
+
 
 		if (_vm->getPlatform() == Common::kPlatformAmiga) {
 			if (!(_vm->getFeatures() & GF_LANG_MULT)) {
@@ -168,15 +190,30 @@ public:
 		}
 
 		int event = _vm->_input->getLastButtonEvent();
-		if (event != kMouseLeftUp) {
-			return this;
-		}
 
 		Common::Point p;
 		_vm->_input->getCursorPos(p);
 
 		for (uint16 i = 0; i < 4; i++) {
 			if (_blocks[i].contains(p)) {
+#ifdef USE_TTS
+				if (_previousBlock != i) {
+					Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+					if (ttsMan != nullptr && ConfMan.getBool("tts_enabled")) {
+						ttsMan->setLanguage(ttsLanguages[i]);
+					}
+
+					_vm->setTTSVoice(kNarratorVoiceID);
+					_vm->sayText(languageSelectBlockTexts[i], Common::TextToSpeechManager::INTERRUPT);
+					_previousBlock = i;
+				}
+#endif
+
+				if (event != kMouseLeftUp) {
+					return this;
+				}
+
+				_vm->stopTextToSpeech();
 				_vm->setInternLanguage(i);
 				_vm->beep();
 				destroyLabels();
@@ -184,6 +221,8 @@ public:
 			}
 		}
 
+		_previousBlock = -1;
+
 		return this;
 	}
 
@@ -259,7 +298,7 @@ public:
 				_vm->_gfx->hideLabel(_labels[_oldChoice]);
 
 			if (_choice != -1)
-				_vm->_gfx->showLabel(_labels[_choice], 60, 30);
+				_vm->_gfx->showLabel(_labels[_choice], 60, 30, false);
 
 			_oldChoice = _choice;
 		}
@@ -312,7 +351,25 @@ public:
 	}
 };
 
+#ifdef USE_TTS
+
+// This text is translated for TTS. Only English is translated in-game
+static const char *ttsIntroMsg[] = {
+	// Italian
+	"Premi il tasto sinistro del mouse per vedere l'introduzione\n"
+	"Premere il tasto destro del mouse per iniziare",
+	// French
+	"Appuyez sur le bouton gauche de la souris pour voir l'intro\n"
+	"Appuyez sur le bouton droit de la souris pour d\351marrer",
+	// English
+	"Press left mouse button to see intro\n"
+	"Press right mouse button to start",
+	// German
+	"Dr\374cken Sie die linke Maustaste, um das Intro anzuzeigen\n"
+	"Zum Starten rechte Maustaste dr\374cken"
+};
 
+#endif
 
 class NewGameInputState_NS : public MenuInputState {
 	Parallaction_ns *_vm;
@@ -338,6 +395,7 @@ public:
 		if (event == kMouseLeftUp || event == kMouseRightUp) {
 			_vm->_input->setMouseState(MOUSE_ENABLED_SHOW);
 			destroyLabels();
+			_vm->stopTextToSpeech();
 
 			if (event == kMouseLeftUp) {
 				_vm->scheduleLocationSwitch("fogne.dough");
@@ -369,14 +427,17 @@ public:
 		_vm->changeBackground("test");
 		_vm->_input->setMouseState(MOUSE_ENABLED_HIDE);
 
+#ifdef USE_TTS
+		_vm->sayText(ttsIntroMsg[_vm->getInternLanguage()], Common::TextToSpeechManager::INTERRUPT);
+#endif
 		_labels[0] = _vm->_gfx->createLabel(_vm->_menuFont, introMsg3[0], 1);
 		_labels[1] = _vm->_gfx->createLabel(_vm->_menuFont, introMsg3[1], 1);
 		_labels[2] = _vm->_gfx->createLabel(_vm->_menuFont, introMsg3[2], 1);
 		_labels[3] = _vm->_gfx->createLabel(_vm->_menuFont, introMsg3[3], 1);
-		_vm->_gfx->showLabel(_labels[0], CENTER_LABEL_HORIZONTAL, 50);
-		_vm->_gfx->showLabel(_labels[1], CENTER_LABEL_HORIZONTAL, 70);
-		_vm->_gfx->showLabel(_labels[2], CENTER_LABEL_HORIZONTAL, 100);
-		_vm->_gfx->showLabel(_labels[3], CENTER_LABEL_HORIZONTAL, 120);
+		_vm->_gfx->showLabel(_labels[0], CENTER_LABEL_HORIZONTAL, 50, false, false);
+		_vm->_gfx->showLabel(_labels[1], CENTER_LABEL_HORIZONTAL, 70, false, false);
+		_vm->_gfx->showLabel(_labels[2], CENTER_LABEL_HORIZONTAL, 100, false, false);
+		_vm->_gfx->showLabel(_labels[3], CENTER_LABEL_HORIZONTAL, 120, false, false);
 	}
 };
 
@@ -676,6 +737,46 @@ const char *SelectCharacterInputState_NS::_charStartLocation[] = {
 	"test.dough"
 };
 
+#ifdef USE_TTS
+
+// Translated text for TTS. Only English is shown in-game
+static const char *italianCredits[] = {
+	"Musica e Effeti Sonori: Marco Caprelli",
+	"Versione PC: Riccardo Ballarino",
+	"Responsabile del progetto: Lovrano Canepa",
+	"Produzione: Bruno Boz",
+	"Sentiti ringraziamenti a: Luigi Benedicenti - Gilda e Danilo",
+	"Copyright 1992 Euclidea s.r.l Italia, tutti i diritti riservati"
+};
+
+static const char *frenchCredits[] = {
+	"Musique et Bruitages: Marco Caprelli",
+	"Version PC: Riccardo Ballarino",
+	"Chef de projet: Lovrano Canepa",
+	"Production: Bruno Boz",
+	"Nous tenons \340 remercier: Luigi Benedicenti - Gilda et Danilo",
+	"Copyright 1992 Euclidea s.r.l Italie, tous droits r\351serv\351s"
+};
+
+static const char *englishCredits[] = {
+	"Music and Sound Effects: Marco Caprelli",
+	"PC Version: Riccardo Ballarino",
+	"Project Manager: Lovrano Canepa",
+	"Production: Bruno Boz",
+	"Special thanks to: Luigi Benedicenti - Gilda and Danilo",
+	"Copyright 1992 Euclidea s.r.l Italy, all rights reserved"
+};
+
+static const char *germanCredits[] = {
+	"Musiken und Soundeffekte: Marco Caprelli",
+	"PC Version: Riccardo Ballarino",
+	"Projektmanager: Lovrano Canepa",
+	"Produktion: Bruno Boz",
+	"Wir danken: Luigi Benedicenti - Gilda und Danilo",
+	"Copyright 1992 Euclidea s.r.l Italien, Alle Rechte vorbehalten"
+};
+
+#endif
 
 class ShowCreditsInputState_NS : public MenuInputState {
 	Parallaction *_vm;
@@ -717,8 +818,31 @@ public:
 
 		_labels[0] = _vm->_gfx->createLabel(_vm->_menuFont, _credits[_current]._role, 1);
 		_labels[1] = _vm->_gfx->createLabel(_vm->_menuFont, _credits[_current]._name, 1);
-		_vm->_gfx->showLabel(_labels[0], CENTER_LABEL_HORIZONTAL, 80);
-		_vm->_gfx->showLabel(_labels[1], CENTER_LABEL_HORIZONTAL, 100);
+		_vm->_gfx->showLabel(_labels[0], CENTER_LABEL_HORIZONTAL, 80, false, false);
+		_vm->_gfx->showLabel(_labels[1], CENTER_LABEL_HORIZONTAL, 100, false, false);
+
+#ifdef USE_TTS
+		const char **creditsTexts;
+		switch (_vm->getInternLanguage()) {
+		case kItalian:
+			creditsTexts = italianCredits;
+			break;
+		case kFrench:
+			creditsTexts = frenchCredits;
+			break;
+		case kEnglish:
+			creditsTexts = englishCredits;
+			break;
+		case kGerman:
+			creditsTexts = germanCredits;
+			break;
+		default:
+			creditsTexts = englishCredits;
+			break;
+		}
+
+		_vm->sayText(creditsTexts[_current], Common::TextToSpeechManager::INTERRUPT);
+#endif
 	}
 
 
@@ -732,7 +856,8 @@ public:
 
 		int event = _vm->_input->getLastButtonEvent();
 		uint32 curTime = _vm->_system->getMillis();
-		if ((event == kMouseLeftUp) || (curTime - _startTime > 5500)) {
+		Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+		if ((event == kMouseLeftUp) || (curTime - _startTime > 5500 && !(ttsMan != nullptr && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()))) {
 			_current++;
 			_startTime = curTime;
 			destroyLabels();
@@ -748,6 +873,7 @@ public:
 	}
 
 	void enter() override {
+		_vm->setTTSVoice(kNarratorVoiceID);
 		_current = -1;
 		_vm->_input->setMouseState(MOUSE_DISABLED);
 	}
@@ -762,6 +888,18 @@ const ShowCreditsInputState_NS::Credit ShowCreditsInputState_NS::_credits[6] = {
 	{"Copyright 1992 Euclidea s.r.l ITALY", "All rights reserved"}
 };
 
+#ifdef USE_TTS
+
+// Only English is translated in-game
+static const char *clickMousePrompts[] = {
+	"Fare clic sul pulsante del mouse per iniziare",		// Italian
+	"Cliquez sur le bouton de la souris pour d\351marrer",	// French
+	"Click mouse button to start",							// English
+	"Klicken Sie mit der Maustaste, um zu starten"			// German
+};
+
+#endif
+
 class EndIntroInputState_NS : public MenuInputState {
 	Parallaction_ns *_vm;
 	bool _isDemo;
@@ -806,7 +944,10 @@ public:
 		if (!_isDemo) {
 			_vm->_soundManI->stopMusic();
 			_label = _vm->_gfx->createLabel(_vm->_menuFont, "CLICK MOUSE BUTTON TO START", 1);
-			_vm->_gfx->showLabel(_label, CENTER_LABEL_HORIZONTAL, 80);
+#ifdef USE_TTS
+			_vm->sayText(clickMousePrompts[_vm->getInternLanguage()], Common::TextToSpeechManager::INTERRUPT);
+#endif
+			_vm->_gfx->showLabel(_label, CENTER_LABEL_HORIZONTAL, 80, false, false);
 		}
 	}
 };
diff --git a/engines/parallaction/metaengine.cpp b/engines/parallaction/metaengine.cpp
index 04d9b7c07f0..70adce018b2 100644
--- a/engines/parallaction/metaengine.cpp
+++ b/engines/parallaction/metaengine.cpp
@@ -43,12 +43,38 @@ Common::Platform Parallaction::getPlatform() const { return _gameDescription->de
 
 } // End of namespace Parallaction
 
+#ifdef USE_TTS
+
+static const ADExtraGuiOptionsMap optionsList[] = {
+	{
+		GAMEOPTION_TTS,
+		{
+			_s("Enable Text to Speech"),
+			_s("Use TTS to read the descriptions (if TTS is available)"),
+			"tts_enabled",
+			false,
+			0,
+			0
+		}
+	},
+	
+	AD_EXTRA_GUI_OPTIONS_TERMINATOR
+};
+
+#endif
+
 class ParallactionMetaEngine : public AdvancedMetaEngine<Parallaction::PARALLACTIONGameDescription> {
 public:
 	const char *getName() const override {
 		return "parallaction";
 	}
 
+#ifdef USE_TTS
+	const ADExtraGuiOptionsMap *getAdvancedExtraGuiOptions() const override {
+		return optionsList;
+	}
+#endif
+
 	bool hasFeature(MetaEngineFeature f) const override;
 	Common::Error createInstance(OSystem *syst, Engine **engine, const Parallaction::PARALLACTIONGameDescription *desc) const override;
 	Common::KeymapArray initKeymaps(const char *target) const override;
diff --git a/engines/parallaction/parallaction.cpp b/engines/parallaction/parallaction.cpp
index 2700ee47458..9d8a504d0d0 100644
--- a/engines/parallaction/parallaction.cpp
+++ b/engines/parallaction/parallaction.cpp
@@ -19,9 +19,11 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/debug-channels.h"
 #include "common/system.h"
 #include "common/textconsole.h"
+#include "common/text-to-speech.h"
 
 #include "parallaction/exec.h"
 #include "parallaction/input.h"
@@ -76,6 +78,7 @@ Parallaction::Parallaction(OSystem *syst, const PARALLACTIONGameDescription *gam
 	_currentLocationIndex = 0;
 	_numLocations = 0;
 	_language = 0;
+	_characterVoiceID = 0;
 }
 
 Parallaction::~Parallaction() {
@@ -116,6 +119,19 @@ Common::Error Parallaction::init() {
 	_location._followerStartFrame = 0;
 	_objects = nullptr;
 
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr) {
+		ttsMan->enable(ConfMan.getBool("tts_enabled"));
+
+		// For the multilingual versions, start with English TTS
+		// The language will be switched later after the user picks their language
+		if (getLanguage() != Common::UNK_LANG) {
+			ttsMan->setLanguage(ConfMan.get("language"));
+		} else {
+			ttsMan->setLanguage("en");
+		}
+	}
+
 	_screenSize = _screenWidth * _screenHeight;
 
 	Common::strcpy_s(_characterName1, "null");
@@ -254,6 +270,8 @@ void Parallaction::showSlide(const char *name, int x, int y) {
 }
 
 void Parallaction::showLocationComment(const Common::String &text, bool end) {
+	setTTSVoice(kNarratorVoiceID);
+	sayText(text, Common::TextToSpeechManager::INTERRUPT);
 	_balloonMan->setLocationBalloon(text, end);
 }
 
@@ -346,6 +364,7 @@ void Parallaction::doLocationEnterTransition() {
 
 	_input->waitForButtonEvent(kMouseLeftUp);
 	_gfx->freeDialogueObjects();
+	setTTSVoice(_characterVoiceID);
 
 	// fades maximum intensity palette towards approximation of main palette
 	for (uint16 _si = 0; _si<6; _si++) {
@@ -792,6 +811,59 @@ ZonePtr Parallaction::hitZone(uint32 type, uint16 x, uint16 y) {
 	return ZonePtr();
 }
 
+void Parallaction::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(text, action, Common::CodePage::kWindows1252);
+	}
+}
+
+void Parallaction::setTTSVoice(int id) const {
+#ifdef USE_TTS
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && _gameType == GType_Nippon) {
+		Common::Array<int> voices;
+		int pitch = 0;
+		Common::TTSVoice::Gender gender;
+
+		if (characterVoiceDatas[id].male) {
+			voices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::MALE);
+			gender = Common::TTSVoice::MALE;
+		} else {
+			voices = ttsMan->getVoiceIndicesByGender(Common::TTSVoice::FEMALE);
+			gender = Common::TTSVoice::FEMALE;
+		}
+
+		// If no voice is available for the necessary gender, set the voice to default
+		if (voices.empty()) {
+			ttsMan->setVoice(0);
+		} else {
+			int voiceIndex = characterVoiceDatas[id].voiceID % voices.size();
+			ttsMan->setVoice(voices[voiceIndex]);
+		}
+
+		// If no voices are available for this gender, alter the pitch to mimic a voice
+		// of the other gender
+		if (ttsMan->getVoice().getGender() != gender) {
+			if (gender == Common::TTSVoice::MALE) {
+				pitch -= 50;
+			} else {
+				pitch += 50;
+			}
+		}
+
+		ttsMan->setPitch(pitch);
+	}
+#endif
+}
+
+void Parallaction::stopTextToSpeech() const {
+	Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+	if (ttsMan != nullptr && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+		ttsMan->stop();
+	}
+}
+
 ZonePtr Location::findZone(const char *name) {
 	for (ZoneList::iterator it = _zones.begin(); it != _zones.end(); ++it) {
 		if (!scumm_stricmp((*it)->_name, name)) return *it;
diff --git a/engines/parallaction/parallaction.h b/engines/parallaction/parallaction.h
index 1cd620dae33..abfeacc9310 100644
--- a/engines/parallaction/parallaction.h
+++ b/engines/parallaction/parallaction.h
@@ -29,6 +29,7 @@
 #include "common/random.h"
 #include "common/savefile.h"
 #include "common/textconsole.h"
+#include "common/text-to-speech.h"
 
 #include "engines/engine.h"
 
@@ -84,6 +85,12 @@ enum {
 	kEvIngameMenu   = 8000
 };
 
+enum LanguageIndex {
+	kItalian = 0,
+	kFrench = 1,
+	kEnglish = 2,
+	kGerman = 3
+};
 
 
 
@@ -219,6 +226,92 @@ public:
 	bool dummy() const;
 };
 
+#ifdef USE_TTS
+
+struct CharacterVoiceData {
+	const char *characterName;
+	uint8 voiceID;
+	bool male;
+};
+
+static const CharacterVoiceData characterVoiceDatas[] = {
+	{ nullptr, 0, true },		// Used as the narrator for cutscene text
+	{ "dough", 1, true },
+	{ "donna", 0, false },
+	{ "dino", 2, true },
+	{ "drki", 3, true },
+	{ "ddd.talk", 1, true },
+	{ nullptr, 0, false },	// Donna in ddd.talk
+	{ nullptr, 2, true },	// Dino in ddd.talk
+	{ "police.talk", 4, true },
+	{ "vecchio.talk", 5, true },
+	{ "karaoke.talk", 6, true },
+	{ "suicida.talk", 7, true },
+	{ "direttore.talk", 8, true },
+	{ "guardiano.talk", 9, true },
+	{ "sento.talk", 10, true },
+	{ "giocatore.talk", 11, true },
+	{ "mona.talk", 12, true },
+	{ "monaco2.talk", 13, true },
+	{ "uccello.talk", 14, true },
+	{ "guru.talk", 15, true },
+	{ "monaco1.talk", 16, true },
+	{ "segretario.talk", 17, true },
+	{ "imperatore.talk", 18, true },
+	{ "secondo.talk", 19, true },
+	{ "taxista.talk", 20, true },
+	{ "bemutsu.talk", 21, true },
+	{ "negro.talk", 22, true },
+	{ "punk.talk", 23, true },
+	{ "chan.talk", 24, true },
+	{ "donna0.talk", 0, false },
+	{ "maxkos.talk", 25, true },
+	{ nullptr, 26, true },		// Second person in the maxkos.talk dialogue
+	{ "drki.talk", 3, true },
+	{ "barman.talk", 27, true },
+	{ "losco.talk", 28, true },
+	{ "mitsu.talk", 29, true },
+	{ "autoradio.talk", 30, true },
+	{ "passa1.talk", 31, true },
+	{ "passa2.talk", 1, false },
+	{ "passa3.talk", 32, true },
+	{ "giornalaio.talk", 33, true },
+	{ "pazza1.talk", 2, false },
+	{ "pazza2.talk", 3, false },
+	{ "pazza3.talk", 4, false },
+	{ "citofono.talk", 34, true },
+	{ "perdelook.talk", 0, false },
+	{ "tele.talk", 35, true },
+	{ "dough.talk", 1, true },
+	{ "donna.talk", 0, false },
+	{ "guanti.talk", 36, true },
+	{ "cuoco.talk", 37, true },
+	{ "punks.talk", 38, true },
+	{ nullptr, 39, true },		// Second person in the punks.talk dialogue
+	{ "punko.talk", 40, true },
+	{ "hotdog.talk", 41, true },
+	{ "cameriera.talk", 5, false },
+	{ "rice1.talk", 42, true },
+	{ nullptr, 43, true },		// Second person in the rice1.talk dialogue
+	{ "segretaria.talk", 6, false },
+	{ "robot.talk", 44, true },
+	{ "portiere.talk", 45, true },
+	{ "figaro.talk", 46, true },
+	{ "ominos.talk", 47, true },
+	{ "cassiera.talk", 7, false },
+	{ "dino.talk", 2, true }
+};
+
+#endif
+
+enum PlayableCharacterVoiceID {
+	kDoug = 1,
+	kDonna = 2,
+	kDino = 3,
+	kDrki = 4
+};
+
+static const int kNarratorVoiceID = 0;
 
 class SaveLoad;
 
@@ -301,6 +394,7 @@ public:
 	Location		_location;
 	ZonePtr			_activeZone;
 	char			_characterName1[50];	// only used in changeCharacter
+	int				_characterVoiceID;
 	ZonePtr			_zoneTrap;
 	ZonePtr			_commentZone;
 	Common::String  _newLocationName;
@@ -359,6 +453,9 @@ public:
 	int16		getInventoryItemIndex(int16 pos);
 	void		openInventory();
 	void		closeInventory();
+	void		sayText(const Common::String &text, Common::TextToSpeechManager::Action action) const;
+	void		setTTSVoice(int id) const;
+	void		stopTextToSpeech() const;
 
 	virtual void parseLocation(const char* name) = 0;
 	virtual void changeLocation() = 0;
@@ -389,6 +486,8 @@ public:
 	uint16			_score;
 	Common::String	_password;
 
+	bool _endCredits;
+
 
 public:
 	void parseLocation(const char *filename) override;
diff --git a/engines/parallaction/parallaction_ns.cpp b/engines/parallaction/parallaction_ns.cpp
index 6c2224df329..053b3f968ed 100644
--- a/engines/parallaction/parallaction_ns.cpp
+++ b/engines/parallaction/parallaction_ns.cpp
@@ -34,6 +34,43 @@
 
 namespace Parallaction {
 
+#ifdef USE_TTS
+
+static const char *openingCreditsFirstLine[] = {
+	"Programmatore Assoluto: Paolo Costabel",	// Italian
+	"Programmeur Absolu: Paolo Costabel",		// French
+	"Absolute Programmer: Paolo Costabel",		// English
+	"Absoluter Programmierer: Paolo Costabel"	// German
+};
+
+// Transcribed for TTS
+// First four names of the end credits are best voiced as one block; the remaining credits
+// are voiced one at a time elsewhere
+static const char *endCreditsFirstSection[] = {
+	// Italian
+	"Il Dottor Buoz\n"
+	"Lo Studente\n"
+	"Il Poliziotto\n"
+	"Secondo il Secondino",
+	// French
+	"Le Professeur Buoz\n"
+	"Le \311tudiant\n"
+	"Le Police\n"
+	"Secondo le Garde",
+	// English
+	"Doctor Buoz\n"
+	"Student\n"
+	"Police\n"
+	"Secondo the Warder",
+	// German
+	"Professor Buoz\n"
+	"Der Student\n"
+	"Die Polizei\n"
+	"Secondo der W\344chter"
+};
+
+#endif
+
 #define INITIAL_FREE_SARCOPHAGUS_SLOT_X	200
 
 
@@ -194,6 +231,7 @@ Common::Error Parallaction_ns::init() {
 
 	_intro = false;
 	_inTestResult = false;
+	_endCredits = false;
 
 	_location._animations.push_front(_char._ani);
 
@@ -382,6 +420,10 @@ void Parallaction_ns::changeLocation() {
 		_input->waitForButtonEvent(kMouseLeftUp);
 		_gfx->unregisterLabel(label);
 		delete label;
+
+		if (!locname.hasCharacter()) {
+			setTTSVoice(_characterVoiceID);
+		}
 	}
 
 	if (locname.hasCharacter()) {
@@ -423,7 +465,21 @@ void Parallaction_ns::changeLocation() {
 		// cave at the end of the game. Fix it here.
 		if (!strcmp(_location._name, "ingressocav"))
 			_input->setMouseState(MOUSE_ENABLED_SHOW);
+#ifdef USE_TTS
+		else if (!strcmp(_location._name, "final")) {	// End credits
+			setTTSVoice(kNarratorVoiceID);
+			sayText(endCreditsFirstSection[getInternLanguage()], Common::TextToSpeechManager::INTERRUPT);
+			_endCredits = true;
+		}
+#endif
 	}
+	
+#ifdef USE_TTS
+	if (!strcmp(_location._name, "title1")) {		// First screen of opening credits
+		setTTSVoice(kNarratorVoiceID);
+		sayText(openingCreditsFirstLine[getInternLanguage()], Common::TextToSpeechManager::INTERRUPT);
+	}
+#endif
 
 	debugC(1, kDebugExec, "changeLocation() done");
 	_newLocationName.clear();
@@ -472,6 +528,18 @@ void Parallaction_ns::changeCharacter(const char *name) {
 
 	_char._ani->gfxobj = _gfx->loadCharacterAnim(_char.getFullName());
 
+	if (scumm_stricmp(_char.getBaseName(), g_doughName) == 0) {
+		_characterVoiceID = kDoug;
+	} else if (scumm_stricmp(_char.getBaseName(), g_donnaName) == 0) {
+		_characterVoiceID = kDonna;
+	} else if (scumm_stricmp(_char.getBaseName(), g_dinoName) == 0) {
+		_characterVoiceID = kDino;
+	} else {
+		_characterVoiceID = kDrki;
+	}
+
+	setTTSVoice(_characterVoiceID);
+
 	if (!_char.dummy()) {
 		_char._head = _disk->loadHead(_char.getBaseName());
 		_char._talk = _disk->loadTalk(_char.getBaseName());




More information about the Scummvm-git-logs mailing list