[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