[Scummvm-git-logs] scummvm master -> 92d487bfd8beb6373a91be89bdd585d3a608df1d

AndywinXp noreply at scummvm.org
Sun Dec 29 13:28:40 UTC 2024


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

Summary:
92d487bfd8 SCUMM: MI1SE: Implement speech entry matching code from disasm


Commit: 92d487bfd8beb6373a91be89bdd585d3a608df1d
    https://github.com/scummvm/scummvm/commit/92d487bfd8beb6373a91be89bdd585d3a608df1d
Author: AndywinXp (andywinxp at gmail.com)
Date: 2024-12-29T14:28:34+01:00

Commit Message:
SCUMM: MI1SE: Implement speech entry matching code from disasm

Changed paths:
    engines/scumm/scumm.h
    engines/scumm/sound.cpp
    engines/scumm/sound.h
    engines/scumm/soundse.cpp
    engines/scumm/soundse.h
    engines/scumm/string.cpp


diff --git a/engines/scumm/scumm.h b/engines/scumm/scumm.h
index 7a7152b2ce9..3c502e701eb 100644
--- a/engines/scumm/scumm.h
+++ b/engines/scumm/scumm.h
@@ -1647,6 +1647,7 @@ protected:
 	virtual bool handleNextCharsetCode(Actor *a, int *c);
 	virtual void drawSentence() {}
 	virtual void displayDialog();
+	int countNumberOfWaits(); // For SE speech support, from disasm
 	bool newLine();
 	void drawString(int a, const byte *msg);
 	virtual void fakeBidiString(byte *ltext, bool ignoreVerb, int ltextSize) const;
diff --git a/engines/scumm/sound.cpp b/engines/scumm/sound.cpp
index c9f74b2aa30..b2282783e5d 100644
--- a/engines/scumm/sound.cpp
+++ b/engines/scumm/sound.cpp
@@ -2192,4 +2192,11 @@ void Sound::updateMusicTimer() {
 	}
 }
 
+void Sound::startRemasteredSpeech(const char *msgString, uint16 roomNumber, uint16 actorTalking, uint16 currentScriptNum, uint16 currentScriptOffset, uint16 numWaits) {
+	// Crudely adapted from the disasm of MI1SE...
+	// TODO: Apply the various speech-line substitutions performed per-game
+
+	_soundSE->handleRemasteredSpeech(msgString, nullptr, roomNumber, actorTalking, currentScriptNum, currentScriptOffset, numWaits);
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/sound.h b/engines/scumm/sound.h
index d03f206bd06..8d3f65b79de 100644
--- a/engines/scumm/sound.h
+++ b/engines/scumm/sound.h
@@ -151,6 +151,7 @@ public:
 	void updateMusicTimer();
 
 	bool useRemasteredAudio() const { return _useRemasteredAudio; }
+	void startRemasteredSpeech(const char *msgString, uint16 roomNumber, uint16 actorTalking, uint16 currentScriptNum, uint16 currentScriptOffset, uint16 numWaits);
 
 	// TODO: Duplicate this in Sound as well?
 	bool isRolandLoom() const { return _soundCD->isRolandLoom(); }
diff --git a/engines/scumm/soundse.cpp b/engines/scumm/soundse.cpp
index fbdb515d33e..e2c54bf396d 100644
--- a/engines/scumm/soundse.cpp
+++ b/engines/scumm/soundse.cpp
@@ -333,6 +333,115 @@ void SoundSE::indexFSBFile(const Common::String &filename, AudioIndex *audioInde
 #undef GET_FSB5_OFFSET
 #undef WARN_AND_RETURN_FSB
 
+static int32 calculateStringHash(const char *input) {
+	int32 hash = 0;
+	int32 multiplier = 0x1EDD;
+
+	for (const char *i = input; *i != '\0'; i++) {
+		char current = *i;
+
+		// Convert lowercase to uppercase...
+		if (current >= 'a' && current <= 'z') {
+			current -= 32;
+		}
+
+		// Process alphanumeric characters only...
+		if ((current >= '0' && current <= '9') ||
+			(current >= 'A' && current <= 'Z')) {
+			multiplier++;
+			hash ^= multiplier * current;
+		}
+	}
+
+	return hash;
+}
+
+static int32 calculate4CharStringHash(const char *str) {
+	int32 hash;
+	int charCount;
+	const char *i;
+	char current;
+
+	hash = 0;
+	charCount = 0;
+
+	// Process until the string terminator or 4 valid characters are found...
+	for (i = str; *i; ++i) {
+		if (charCount >= 4)
+			break;
+
+		current = *i;
+
+		if ((current >= 'A' && current <= 'Z') || (current >= 'a' && current <= 'z')) {
+			// Take the lower nibble of the char and incorporate it into the hash...
+			hash = (16 * hash) | current & 0xF;
+			++charCount;
+		}
+	}
+
+	return hash;
+}
+
+static int32 calculateStringSimilarity(const char *str1, const char *str2) {
+	// This function is responsible for calculating a similarity score between
+	// the two input strings; the closer the score is to zero, the closer the
+	// two strings are. Taken from disasm.
+
+	int str1Len = strlen(str1);
+	int str2Len = strlen(str2);
+	int totalPenalty = 0;
+	int lastMatchOffset = 0;
+
+	// Return 0 if first string is empty...
+	if (str1Len <= 0)
+		return 0;
+
+	// Scan through first string with a sliding window...
+	for (int windowPos = 3; windowPos - 3 < str1Len; windowPos++) {
+		char currentChar = str1[windowPos - 3];
+
+		// Check if the current character is alphanumeric...
+		if ((currentChar >= 'a' && currentChar <= 'z') || (currentChar >= 'A' && currentChar <= 'Z') ||
+			(currentChar >= '0' && currentChar <= '9')) {
+
+			// Normalize character to 5-bit value (so that it's case insensitive)
+			char normalizedChar = currentChar & 0x1F;
+			int penalty = 9; // Default penalty
+
+			// Calculate the search window bounds in the second string...
+			int searchStart = (windowPos - 6 <= 0) ? 0 : windowPos - 6;
+			int searchEnd = windowPos;
+
+			// Look for matching character in second string...
+			if (searchStart <= searchEnd) {
+				while (searchStart < str2Len) {
+					if ((str2[searchStart] & 0x1F) == normalizedChar) {
+						int positionDiff = windowPos - searchStart - 3;
+
+						// If character found at same relative position as last match...
+						if (lastMatchOffset == positionDiff) {
+							penalty = 0; // No penalty for consistent positioning!
+						} else {
+							// Penalty based on square of position difference!
+							penalty = positionDiff * positionDiff;
+							lastMatchOffset = positionDiff;
+						}
+
+						break;
+					}
+
+					if (++searchStart > searchEnd)
+						break;
+				}
+			}
+
+			totalPenalty -= penalty; // Subtract penalty from total score...
+		}
+	}
+
+	return totalPenalty;
+}
+
 void SoundSE::initAudioMappingMI() {
 	Common::File *f = new Common::File();
 	if (!f->open(Common::Path("speech.info"))) {
@@ -340,6 +449,8 @@ void SoundSE::initAudioMappingMI() {
 		return;
 	}
 
+	_audioEntriesMI.clear();
+
 	do {
 		AudioEntryMI entry;
 		entry.hash = f->readUint32LE();
@@ -357,7 +468,9 @@ void SoundSE::initAudioMappingMI() {
 		entry.textSpanish = f->readString(0, 256);
 
 		entry.speechFile  = f->readString(0, 32);
-		entry.speechFile.toLowercase();
+		//entry.speechFile.toLowercase();
+
+		entry.hashFourCharString = calculate4CharStringHash(entry.textEnglish.c_str()); // From disasm
 
 		//debug("hash %d, room %d, script %d, localScriptOffset: %d, messageIndex %d, isEgoTalking: %d, wait: %d, textEnglish '%s', speechFile '%s'",
 		//	  entry.hash, entry.room, entry.script,
@@ -371,7 +484,7 @@ void SoundSE::initAudioMappingMI() {
 			entry.messageIndex
 		);
 
-		//_audioEntriesMI.push_back(entry);
+		_audioEntriesMI.emplace_back(entry);
 	} while (!f->eos());
 
 	f->close();
@@ -482,6 +595,71 @@ int32 SoundSE::getSoundIndexFromOffset(uint32 offset) {
 		return -1;
 }
 
+SoundSE::AudioEntryMI *SoundSE::getAppropriateSpeechCue(const char *msgString, const char *speechFilenameSubstitution,
+											   uint16 roomNumber, uint16 actorTalking, uint16 scriptNum, uint16 scriptOffset, uint16 numWaits) {
+	uint32 hash;
+	AudioEntryMI *curAudioEntry;
+	uint16 script;
+	int32 currentScore;
+	int32 bestScore;
+	int bestScoreIdx;
+	uint32 tmpHash;
+
+	hash = calculateStringHash(msgString);
+
+	tmpHash = hash;
+	if (!hash)
+		return nullptr;
+
+	bestScore = 0x40000000; // This is the score that we have to minimize...
+	bestScoreIdx = -1;
+
+	if (_audioEntriesMI.empty())
+		return nullptr;
+
+	for (uint curEntryIdx = 0; curEntryIdx < _audioEntriesMI.size(); curEntryIdx++) {
+		curAudioEntry = &_audioEntriesMI[curEntryIdx];
+
+		if (curAudioEntry->hash == hash &&
+			curAudioEntry->messageIndex == numWaits &&
+			calculate4CharStringHash(msgString) == curAudioEntry->hashFourCharString) {
+
+			currentScore = ABS(scriptOffset - curAudioEntry->localScriptOffset - 7);
+			if (curAudioEntry->room == roomNumber) {
+				script = curAudioEntry->script;
+				if (script && script != scriptNum)
+					currentScore = 10000;
+			} else {
+				currentScore += 10000;
+			}
+
+			currentScore -= 10 * calculateStringSimilarity(curAudioEntry->textEnglish.c_str(), msgString);
+
+			if (actorTalking == 255) {
+				if (curAudioEntry->isEgoTalking == 1)
+					currentScore += 2000;
+			} else if ((actorTalking == 1) != curAudioEntry->isEgoTalking) {
+				currentScore += 20000;
+			}
+
+			if (speechFilenameSubstitution &&
+				scumm_strnicmp(curAudioEntry->speechFile.c_str(), speechFilenameSubstitution, strlen(speechFilenameSubstitution))) {
+				currentScore += 100000;
+			}
+			if (currentScore < bestScore) {
+				bestScore = currentScore;
+				bestScoreIdx = (int)curEntryIdx;
+			}
+		}
+
+		hash = tmpHash;
+	}
+	if (bestScoreIdx == -1)
+		return nullptr;
+	else
+		return &_audioEntriesMI[bestScoreIdx];
+}
+
 Audio::AudioStream *SoundSE::getAudioStream(uint32 offset, SoundSEType type) {
 	Common::SeekableReadStream *stream;
 	Common::String audioFileName;
@@ -542,4 +720,101 @@ uint32 SoundSE::getAudioOffsetForMI(int32 room, int32 script, int32 localScriptO
 	return ((room + script + messageIndex) << 16) | (localScriptOffset & 0xFFFF);
 }
 
+Common::String calculateCurrentString(const char *msgString) {
+	char currentChar;
+	bool shouldContinue = true;
+	char messageBuffer[512];
+	char *outMsgBuffer = messageBuffer;
+
+	memset(messageBuffer, 0, sizeof(messageBuffer));
+
+	currentChar = *msgString;
+
+	// Handle empty string case
+	if (msgString[0] == '\0') {
+		messageBuffer[0] = 0;
+		// TODO: This case sets a variable msgType equal to 1,
+		// it's not clear where this is used in the executable...
+	} else {
+		// Process each character to find the control codes...
+		while (shouldContinue && *msgString) {
+			currentChar = *msgString;
+
+			// If there are no control codes...
+			if (currentChar != (char)0xFF && currentChar != (char)0xFE) {
+				// Handle normal characters...
+				switch (currentChar) {
+				case '\r':
+					*outMsgBuffer = '\n';
+					outMsgBuffer++;
+					msgString++;
+					break;
+
+				case '@':
+				case 8: // "Verb next line" marker...
+					msgString++;
+					break;
+
+				default: // Normal character, copy it over...
+					*outMsgBuffer = *msgString;
+					outMsgBuffer++;
+					msgString++;
+					break;
+				}
+			} else {
+				// Handle special character sequences
+				msgString++;
+				currentChar = *msgString;
+
+				switch (currentChar) {
+				case 1: // "Next line" marker
+					*outMsgBuffer = '\n';
+					outMsgBuffer++;
+					msgString++;
+					break;
+
+				case 2: // "No crlf" marker
+					shouldContinue = false;
+					// TODO: This case sets a variable msgType equal to 2,
+					// it's not clear where this is used in the executable...
+					*outMsgBuffer = '\0';
+					break;
+
+				case 3: // "Wait" marker
+					*outMsgBuffer = '\0';
+					// TODO: This case sets a variable msgType equal to 1,
+					// it's not clear where this is used in the executable...
+					shouldContinue = false;
+					break;
+
+				default:
+					break; // Do nothing
+				}
+			}
+		}
+
+		// Handle end of string if we haven't already
+		if (shouldContinue) {
+			*outMsgBuffer = '\0';
+			// TODO: This case sets a variable msgType equal to 1,
+			// it's not clear where this is used in the executable...
+		}
+	}
+
+	Common::String result(messageBuffer);
+
+	return result;
+}
+
+void SoundSE::handleRemasteredSpeech(const char *msgString, const char *speechFilenameSubstitution,
+								     uint16 roomNumber, uint16 actorTalking, uint16 scriptNum, uint16 scriptOffset, uint16 numWaits) {
+
+	// Get the string without the various control codes and special characters...
+	Common::String currentString = calculateCurrentString(msgString);
+	AudioEntryMI *entry = getAppropriateSpeechCue(currentString.c_str(), speechFilenameSubstitution, roomNumber, actorTalking, scriptNum, scriptOffset, numWaits);
+	if (entry)
+		debug("Selected entry: %s (%s)", entry->textEnglish.c_str(), entry->speechFile.c_str());
+
+}
+
 } // End of namespace Scumm
diff --git a/engines/scumm/soundse.h b/engines/scumm/soundse.h
index ab24059fe4c..42d344ee502 100644
--- a/engines/scumm/soundse.h
+++ b/engines/scumm/soundse.h
@@ -47,6 +47,25 @@ enum SoundSEType {
 class SoundSE {
 
 protected:
+	// Used in MI1 + MI2
+	struct AudioEntryMI {
+		uint32 hash;
+		uint16 room;
+		uint16 script;
+		uint16 localScriptOffset;
+		uint16 messageIndex;        // message index, used in messages split with wait()
+		uint16 isEgoTalking;        // 1 if ego is talking, 0 otherwise
+		uint16 wait;                // wait time in ms
+		Common::String textEnglish; // 256 bytes, English text
+		Common::String textFrench;  // 256 bytes, French text
+		Common::String textItalian; // 256 bytes, Italian text
+		Common::String textGerman;  // 256 bytes, German text
+		Common::String textSpanish; // 256 bytes, Spanish text
+		Common::String speechFile;  // 32 bytes
+
+		int32 hashFourCharString; // Hash calculated on a four char string, from disasm
+	};
+
 	ScummEngine *_vm;
 	Audio::Mixer *_mixer;
 
@@ -58,6 +77,22 @@ public:
 	Audio::AudioStream *getAudioStream(uint32 offset, SoundSEType type);
 	uint32 getAudioOffsetForMI(int32 room, int32 script, int32 localScriptOffset, int32 messageIndex);
 
+	void handleRemasteredSpeech(const char *msgString,
+								const char *speechFilenameSubstitution,
+								uint16 roomNumber,
+								uint16 actorTalking,
+								uint16 scriptNum,
+								uint16 scriptOffset,
+								uint16 numWaits);
+
+	AudioEntryMI *getAppropriateSpeechCue(const char *msgString,
+										  const char *speechFilenameSubstitution,
+										  uint16 roomNumber,
+										  uint16 actorTalking,
+										  uint16 scriptNum,
+										  uint16 scriptOffset,
+										  uint16 numWaits);
+
 private:
 	enum AudioCodec {
 		kXWBCodecPCM = 0,
@@ -100,25 +135,8 @@ private:
 	AudioIndex _sfxEntries;
 	Common::String _sfxFilename;
 
-	// Used in MI1 + MI2
-	struct AudioEntryMI {
-		uint32 hash;
-		uint16 room;
-		uint16 script;
-		uint16 localScriptOffset;
-		uint16 messageIndex;        // message index, used in messages split with wait()
-		uint16 isEgoTalking;        // 1 if ego is talking, 0 otherwise
-		uint16 wait;                // wait time in ms
-		Common::String textEnglish; // 256 bytes, English text
-		Common::String textFrench;  // 256 bytes, French text
-		Common::String textItalian; // 256 bytes, Italian text
-		Common::String textGerman;  // 256 bytes, German text
-		Common::String textSpanish; // 256 bytes, Spanish text
-		Common::String speechFile;  // 32 bytes
-	};
-
-	//typedef Common::Array<AudioEntryMI> AudioIndexMI;
-	//AudioIndexMI _audioEntriesMI;
+	typedef Common::Array<AudioEntryMI> AudioIndexMI;
+	AudioIndexMI _audioEntriesMI;
 
 	int32 getSoundIndexFromOffset(uint32 offset);
 
diff --git a/engines/scumm/string.cpp b/engines/scumm/string.cpp
index fbb054e6a90..36384a88d6c 100644
--- a/engines/scumm/string.cpp
+++ b/engines/scumm/string.cpp
@@ -1115,7 +1115,18 @@ void ScummEngine::displayDialog() {
 		fakeBidiString(_charsetBuffer + _charsetBufPos, true, sizeof(_charsetBuffer) - _charsetBufPos);
 
 	if ((_game.features & GF_DOUBLEFINE_PAK) && _game.id == GID_MONKEY && _sound->useRemasteredAudio()) {
-		_sound->talkSound(_currentScriptOffsetSavedForSpeechMI, 0, DIGI_SND_MODE_TALKIE);
+		//_sound->talkSound(_currentScriptOffsetSavedForSpeechMI, 0, DIGI_SND_MODE_TALKIE);
+		int numberOfWaits = countNumberOfWaits();
+		int32 currentActor = VAR_TALK_ACTOR != 0xFF ? VAR(VAR_TALK_ACTOR) : 0;
+
+		// Explicitly truncate all relevant params to uint16! This is intentional!
+		_sound->startRemasteredSpeech(
+			(const char *)&_charsetBuffer[_charsetBufPos],
+			(uint16)_currentRoom,
+			(uint16)currentActor,
+			(uint16)_currentScriptSavedForSpeechMI,
+			(uint16)_currentScriptOffsetSavedForSpeechMI,
+			(uint16)numberOfWaits);
 	}
 
 	bool createTextBox = (_macGui && _game.id == GID_INDY3);
@@ -1213,6 +1224,27 @@ void ScummEngine::displayDialog() {
 #endif
 }
 
+int ScummEngine::countNumberOfWaits() {
+	int idx, numWaits;
+	byte curChar;
+
+	idx = 0;
+	numWaits = 0;
+
+	if (_charsetBufPos) {
+		do {
+			curChar = _charsetBuffer[idx++];
+			if (curChar == 0xFF || curChar == 0xFE) {
+				if (_charsetBuffer[idx] == 3)
+					++numWaits;
+				++idx;
+			}
+		} while (idx < _charsetBufPos);
+	}
+
+	return numWaits;
+}
+
 void ScummEngine::drawString(int a, const byte *msg) {
 	byte buf[270];
 	byte *space;




More information about the Scummvm-git-logs mailing list