scummvm/engines/scumm/soundse.cpp
Filippos Karapetis d400bb5833 SCUMM: WIP code for headerless WMA streams used in MI1:SE and MI2:SE
It still doesn't sound right, so it's disabled, for now
2025-02-05 10:57:25 +02:00

991 lines
28 KiB
C++

/* 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/>.
*
*/
#include "common/config-manager.h"
#include "common/timer.h"
#include "common/util.h"
#include "common/substream.h"
#include "scumm/file.h"
#include "scumm/scumm.h"
#include "scumm/soundse.h"
#include "audio/audiostream.h"
#include "audio/decoders/adpcm.h"
#include "audio/decoders/mp3.h"
#include "audio/decoders/raw.h"
#include "audio/decoders/wma.h"
namespace Scumm {
SoundSE::SoundSE(ScummEngine *parent, Audio::Mixer *mixer)
: _vm(parent),
_mixer(mixer) {
initSoundFiles();
}
void SoundSE::initSoundFiles() {
switch (_vm->_game.id) {
case GID_MONKEY:
case GID_MONKEY2:
initAudioMappingMI();
indexXWBFile(kSoundSETypeMusic);
indexXWBFile(kSoundSETypeSFX);
indexXWBFile(kSoundSETypeSpeech);
indexXWBFile(kSoundSETypeAmbience);
if (_vm->_game.id == GID_MONKEY2) {
indexXWBFile(kSoundSETypeCommentary);
// We need the speechcues.xsb file for MI2's speech,
// since the file names, which are used to match the
// speech cues with the audio files, are stored in there.
indexSpeechXSBFile();
// Patch audio files. Since this relies on file names,
// it needs to be called after the file names are defined
// from the speech cues above.
indexXWBFile(kSoundSETypePatch);
}
break;
case GID_TENTACLE:
case GID_FT:
initAudioMappingDOTTAndFT();
indexFSBFile(kSoundSETypeMusic);
indexFSBFile(kSoundSETypeSFX);
indexFSBFile(kSoundSETypeSpeech);
indexFSBFile(kSoundSETypeCommentary);
// TODO: iMUSEClient_SFX_STREAMING.fsb for FT
// Clear the original offset map, as we no longer need it
_nameToOffsetDOTTAndFT.clear();
break;
default:
error("initSoundFiles: unhandled game");
}
}
#define WARN_AND_RETURN_XWB(message) \
{ \
warning("indexXWBFile: %s", message); \
delete f; \
return; \
}
void SoundSE::indexXWBFile(SoundSEType type) {
// This implementation is based off unxwb: https://github.com/mariodon/unxwb/
// as well as xwbdump: https://raw.githubusercontent.com/wiki/Microsoft/DirectXTK/xwbdump.cpp
// Only the parts that apply to the Special Editions of
// MI1 and MI2 have been implemented.
struct SegmentData {
uint32 offset;
uint32 length;
};
SegmentData segments[5] = {};
AudioIndex *audioIndex = getAudioEntries(type);
Common::SeekableReadStream *f = getAudioFile(type);
if (!f)
return;
const uint32 magic = f->readUint32BE();
const uint32 version = f->readUint32LE();
f->skip(4); // skip dwHeaderVersion
if (magic != MKTAG('W', 'B', 'N', 'D'))
WARN_AND_RETURN_XWB("Invalid XWB file")
if (version < 42)
WARN_AND_RETURN_XWB("Unsupported XWB version")
for (uint32 i = 0; i < 5; i++) {
segments[i].offset = f->readUint32LE();
segments[i].length = f->readUint32LE();
}
f->seek(segments[kXWBSegmentBankData].offset);
const uint32 flags = f->readUint32LE();
const uint32 entryCount = f->readUint32LE();
f->skip(64); // skip bank name
const uint32 entrySize = f->readUint32LE();
if (entrySize < 24)
WARN_AND_RETURN_XWB("Unsupported XWB entry size")
if (flags & 0x00020000)
WARN_AND_RETURN_XWB("XWB compact format is not supported")
f->seek(segments[kXWBSegmentEntryMetaData].offset);
for (uint32 i = 0; i < entryCount; i++) {
AudioEntry entry;
/*uint32 flagsAndDuration = */ f->readUint32LE();
uint32 format = f->readUint32LE();
entry.offset = f->readUint32LE() + segments[kXWBSegmentEntryWaveData].offset;
entry.length = f->readUint32LE();
/*uint32 loopOffset = */ f->readUint32LE();
/*uint32 loopLength = */ f->readUint32LE();
entry.codec = static_cast<AudioCodec>(format & ((1 << 2) - 1));
entry.channels = (format >> (2)) & ((1 << 3) - 1);
entry.rate = (format >> (2 + 3)) & ((1 << 18) - 1);
entry.align = (format >> (2 + 3 + 18)) & ((1 << 8) - 1);
entry.bits = (format >> (2 + 3 + 18 + 8)) & ((1 << 1) - 1);
entry.isPatched = false;
audioIndex->push_back(entry);
}
const uint32 nameOffset = segments[kXWBSegmentEntryNames].offset;
if (nameOffset) {
f->seek(nameOffset);
for (uint32 i = 0; i < entryCount; i++) {
Common::String name = f->readString(0, 64);
name.toLowercase();
if (type == kSoundSETypeSpeech) {
(*audioIndex)[i].name = name;
_nameToIndexMISpeech[name] = i;
} else if (type == kSoundSETypePatch) {
// Patch audio resources for MI2
// Note: We assume that patch XWB files always contain file names
// In Monkey Island 2, there's a gag with a phone operator from
// the LucasArts help line, Chester, who responds to a call from
// a phone located inside the Dinky Island jungle (room 155, boot
// param 996). In the classic version, Chester was female, but was
// replaced by a male operator in the Special Edition. The original
// audio files for Chester are "chf_97_*, and the new audio files
// are che_97_*. We patch the female voice for Chester's sound files
// here.
if (name.hasPrefix("chf_97_jungleb_")) {
name.setChar('e', 2);
}
// Note: The original patch also contained the following entries:
// - Fixes for audio sync during the skeleton dance / dream
// sequence (boot param 675). These are not needed for the
// classic version, and only apply to the Special Edition.
// - Missing music files for Dinky Jungle. We don't use these
// yet, so we don't patch them.
// TODO: Process and patch music entries, once we start using
// the SE audio files for music.
const int32 originalAudioIndex = _nameToIndexMISpeech[name];
if (originalAudioIndex < (int32)_speechEntries.size() && _speechEntries[originalAudioIndex].name == name) {
_speechEntries[originalAudioIndex].isPatched = true;
_nameToIndexMISpeechPatched[name] = i;
}
}
}
}
delete f;
}
#undef WARN_AND_RETURN_XWB
void SoundSE::indexSpeechXSBFile() {
Common::List<uint16> speechIndices;
AudioIndex *audioIndex = getAudioEntries(kSoundSETypeSpeech);
Common::SeekableReadStream *f = getAudioFile("speechcues.xsb");
if (!f)
return;
const uint32 magic = f->readUint32BE();
if (magic != MKTAG('S', 'D', 'B', 'K')) {
warning("Invalid XSB file");
delete f;
return;
}
f->skip(15);
const uint32 entryCount = f->readUint32LE();
f->skip(19);
const uint32 nameOffset = f->readUint32LE();
f->skip(24);
const uint32 entriesOffset = f->readUint32LE();
f->seek(entriesOffset);
for (uint32 i = 0; i < entryCount; i++) {
uint16 entryTag = f->readUint16LE();
bool isSpeech = (entryTag == 0x0410);
f->skip(7);
const uint16 speechIndex = f->readUint16LE();
speechIndices.push_back(speechIndex);
//debug("indexSpeechXSBFile: speech cue %d -> index %d, offset %d", i, speechIndex, f->pos());
f->skip(isSpeech ? 8 : 1);
}
f->seek(nameOffset);
for (auto &index : speechIndices) {
Common::String name = f->readString(0);
name.toLowercase();
if (index < (*audioIndex).size()) {
(*audioIndex)[index].name = name;
_nameToIndexMISpeech[name] = index;
//debug("indexSpeechXSBFile: %s -> index %d", name.c_str(), index);
}
}
delete f;
}
#define GET_FSB5_OFFSET(X) ((((X) >> (uint64)7) << (uint64)5) & (((uint64)1 << (uint64)32) - 1))
void SoundSE::indexFSBFile(SoundSEType type) {
// Based off DoubleFine Explorer: https://github.com/bgbennyboy/DoubleFine-Explorer/blob/master/uDFExplorer_FSBManager.pas
// and fsbext: https://aluigi.altervista.org/search.php?src=fsbext
AudioIndex *audioIndex = getAudioEntries(type);
Common::SeekableReadStream *f = getAudioFile(type);
if (!f)
return;
const uint32 headerSize = 60; // 4 * 7 + 8 + 16 + 8
const uint32 magic = f->readUint32BE();
if (magic != MKTAG('F', 'S', 'B', '5')) {
warning("Invalid FSB file");
delete f;
return;
}
/*const uint32 version = */f->readUint32LE();
const uint32 sampleCount = f->readUint32LE();
const uint32 sampleHeaderSize = f->readUint32LE();
const uint32 nameSize = f->readUint32LE();
const uint32 dataSize = f->readUint32LE();
/*const uint32 mode = */f->readUint32LE();
f->skip(8); // skip zero
f->skip(16); // skip hash
f->skip(8); // skip dummy
const uint32 nameOffset = sampleHeaderSize + headerSize;
const uint32 baseOffset = headerSize + sampleHeaderSize + nameSize;
uint64 pos = f->pos();
f->seek(nameOffset);
const uint32 firstNameOffset = nameOffset + f->readUint32LE();
f->seek(pos);
for (uint32 i = 0; i < sampleCount; i++) {
const uint32 origOffset = f->readUint32LE();
f->skip(4); // samples, used in XMA
uint32 sampleType = origOffset & ((1 << 7) - 1);
const uint32 fileOffset = nameOffset + nameSize + GET_FSB5_OFFSET(origOffset);
uint32 size;
// Meta data, skip it
while (sampleType & 1) {
const uint32 t = f->readUint32LE();
sampleType = t & 1;
const uint32 metaDataSize = (t & 0xffffff) >> 1;
f->skip(metaDataSize);
}
if (f->pos() < nameOffset) {
size = f->readUint32LE();
f->seek(-4, SEEK_CUR);
if (!size) {
size = dataSize + baseOffset;
} else {
size = GET_FSB5_OFFSET(size) + baseOffset;
}
} else {
size = dataSize + baseOffset;
}
size -= fileOffset;
AudioEntry entry;
entry.length = size;
entry.offset = fileOffset;
// The following are all unused - they'll
// be read from the MP3 streams
entry.rate = 48000;
entry.channels = 2;
entry.codec = kFSBCodecMP3;
entry.align = 0;
entry.bits = 16;
entry.isPatched = false;
audioIndex->push_back(entry);
}
f->seek(firstNameOffset);
for (uint32 i = 0; i < sampleCount; i++) {
Common::String name = f->readString();
name.toLowercase();
// Ignore sound files for the SE in-game UI
if (name.hasPrefix("ui_") || name.hasPrefix("ft_front_end_"))
continue;
// Ignore seemingly duplicate audio files in FT
if (name.hasSuffix("-copy"))
continue;
// TODO: Support non-English audio files
if (name.hasPrefix("de_") || name.hasPrefix("fr_") || name.hasPrefix("it_"))
continue;
if (name.hasPrefix("en_") || name.hasPrefix("de_") || name.hasPrefix("fr_") || name.hasPrefix("it_"))
name = name.substr(3);
// Ignore classic files and use the HQ ones
if (name.hasPrefix("classic_"))
continue;
if (name.hasPrefix("hq_"))
name = name.substr(3);
(*audioIndex)[i].name = name;
if (!_nameToOffsetDOTTAndFT.contains(name)) {
//warning("indexFSBFile: name %s not found in audiomapping.info", name.c_str());
continue;
}
const uint32 origOffset = _nameToOffsetDOTTAndFT[name];
_offsetToIndexDOTTAndFT[origOffset] = i;
//debug("indexFSBFile: %s -> offset %d, index %d", name.c_str(), origOffset, i);
}
delete f;
}
#undef GET_FSB5_OFFSET
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::SeekableReadStream *f = getAudioFile("speech.info");
if (!f)
return;
_audioEntriesMI.clear();
do {
AudioEntryMI entry;
entry.hash = f->readUint32LE();
entry.room = f->readUint16LE();
entry.script = f->readUint16LE();
entry.localScriptOffset = f->readUint16LE();
entry.messageIndex = f->readUint16LE();
entry.isEgoTalking = f->readUint16LE();
entry.wait = f->readUint16LE();
entry.textEnglish = f->readString(0, 256);
entry.textFrench = f->readString(0, 256);
entry.textItalian = f->readString(0, 256);
entry.textGerman = f->readString(0, 256);
entry.textSpanish = f->readString(0, 256);
entry.speechFile = f->readString(0, 32);
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,
// entry.localScriptOffset, entry.messageIndex, entry.isEgoTalking, entry.wait,
// entry.textEnglish.c_str(), entry.speechFile.c_str());
_audioEntriesMI.emplace_back(entry);
} while (!f->eos());
delete f;
}
void SoundSE::initAudioMappingDOTTAndFT() {
Common::SeekableReadStream *f = getAudioFile("audiomapping.info");
if (!f)
return;
do {
const uint32 origOffset = f->readUint32LE();
Common::String name = f->readString(0, 64);
name.toLowercase();
if (f->eos())
break;
f->skip(4); // unknown flag
if (_vm->_game.id == GID_FT)
f->skip(4); // unknown flag
_nameToOffsetDOTTAndFT[name] = origOffset;
} while (!f->eos());
delete f;
}
Common::String SoundSE::getAudioFilename(SoundSEType type) {
const bool isMonkey = _vm->_game.id == GID_MONKEY || _vm->_game.id == GID_MONKEY2;
const bool isTentacle = _vm->_game.id == GID_TENTACLE;
const bool isFT = _vm->_game.id == GID_FT;
switch (type) {
case kSoundSETypeMusic:
case kSoundSETypeCDAudio:
return isMonkey ? "MusicOriginal.xwb" : "iMUSEClient_Music.fsb";
case kSoundSETypeSpeech:
if (isMonkey)
return "Speech.xwb";
else if (isTentacle)
return "iMUSEClient_VO.fsb";
else if (isFT)
return "iMUSEClient_SPEECH.fsb";
else
error("getAudioFilename: unknown game type in SoundSEType %d", type);
break;
case kSoundSETypeSFX:
if (isMonkey)
return "SFXOriginal.xwb";
else if (isTentacle)
return "iMUSEClient_SFX.fsb";
else if (isFT)
return "iMUSEClient_SFX_INMEMORY.fsb";
else
error("getAudioFilename: unknown game type in SoundSEType %d", type);
break;
case kSoundSETypeAmbience:
return "Ambience.xwb";
case kSoundSETypeCommentary:
return isMonkey ? "commentary.xwb" : "iMUSEClient_Commentary.fsb";
case kSoundSETypePatch:
return "patch.xwb";
default:
error("getAudioFilename: unknown SoundSEType %d", type);
break;
}
}
Common::SeekableReadStream *SoundSE::getAudioFile(SoundSEType type) {
Common::String audioFileName = getAudioFilename(type);
return getAudioFile(audioFileName);
}
Common::SeekableReadStream *SoundSE::getAudioFile(const Common::String &filename) {
if (_vm->_game.id == GID_MONKEY || _vm->_game.id == GID_MONKEY2) {
Common::File *audioFile = new Common::File();
if (!audioFile->open(Common::Path(filename))) {
warning("getAudioFile: failed to open %s", filename.c_str());
delete audioFile;
return nullptr;
}
return audioFile;
} else {
ScummPAKFile *audioFile = new ScummPAKFile(_vm);
if (!_vm->openFile(*audioFile, Common::Path(filename))) {
warning("getAudioFile: failed to open %s", filename.c_str());
delete audioFile;
return nullptr;
}
return audioFile;
}
}
SoundSE::AudioIndex *SoundSE::getAudioEntries(SoundSEType type) {
switch (type) {
case kSoundSETypeMusic:
case kSoundSETypeCDAudio:
return &_musicEntries;
case kSoundSETypeSpeech:
return &_speechEntries;
case kSoundSETypeSFX:
return &_sfxEntries;
case kSoundSETypeAmbience:
return &_ambienceEntries;
case kSoundSETypeCommentary:
return &_commentaryEntries;
case kSoundSETypePatch:
return &_patchEntries;
default:
error("getAudioEntries: unknown SoundSEType %d", type);
}
}
Audio::SeekableAudioStream *SoundSE::createSoundStream(Common::SeekableSubReadStream *stream, AudioEntry entry, DisposeAfterUse::Flag disposeAfterUse) {
switch (entry.codec) {
case kXWBCodecPCM: {
byte flags = Audio::FLAG_LITTLE_ENDIAN;
if (entry.bits == 1) // 0: 8 bits, 1: 16 bits
flags |= Audio::FLAG_16BITS;
if (entry.channels == 2)
flags |= Audio::FLAG_STEREO;
return Audio::makeRawStream(stream, entry.rate, flags, disposeAfterUse);
}
case kXWBCodecXMA:
// Unused in MI1SE and MI2SE
error("createSoundStream: XMA codec not supported");
case kXWBCodecADPCM: {
const uint32 blockAlign = (entry.align + 22) * entry.channels;
return Audio::makeADPCMStream(
stream,
disposeAfterUse,
entry.length,
Audio::kADPCMMS,
entry.rate,
entry.channels,
blockAlign
);
}
case kXWBCodecWMA:
// TODO: Implement WMA codec
warning("createSoundStream: WMA codec not implemented");
delete stream;
return nullptr;
#if 0
return new HeaderlessWMAStream(stream, entry, disposeAfterUse);
#endif
case kFSBCodecMP3:
#ifdef USE_MAD
return Audio::makeMP3Stream(
stream,
disposeAfterUse
);
#else
warning("createSoundStream: MP3 codec is not built in");
delete stream;
return nullptr;
#endif
}
error("createSoundStream: Unknown XWB codec %d", entry.codec);
}
int32 SoundSE::getSoundIndexFromOffset(uint32 offset) {
if (_vm->_game.id == GID_MONKEY || _vm->_game.id == GID_MONKEY2) {
return offset;
} else if (_vm->_game.id == GID_TENTACLE || _vm->_game.id == GID_FT) {
return (_offsetToIndexDOTTAndFT.contains(offset)) ? (int32)_offsetToIndexDOTTAndFT[offset] : -1;
}
return -1;
}
int32 SoundSE::getAppropriateSpeechCue(const char *msgString, const char *speechFilenameSubstitution,
uint16 roomNumber, uint16 actorTalking, uint16 scriptNum, uint16 scriptOffset, uint16 numWaits) {
uint32 hash = calculateStringHash(msgString);
uint32 tmpHash = hash;
AudioEntryMI *curAudioEntry;
uint16 script;
int32 currentScore;
int32 bestScore = 0x40000000; // This is the score that we have to minimize...
int32 bestScoreIdx = -1;
if (!hash || _audioEntriesMI.empty())
return -1;
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 = (int32)curEntryIdx;
}
}
hash = tmpHash;
}
return bestScoreIdx;
}
Audio::SeekableAudioStream *SoundSE::getAudioStreamFromOffset(uint32 offset, SoundSEType type) {
int32 index = getSoundIndexFromOffset(offset);
if (index < 0) {
warning("getAudioStreamFromOffset: sound index not found for offset %d", offset);
return nullptr;
}
return getAudioStreamFromIndex(index, type);
}
Audio::SeekableAudioStream *SoundSE::getAudioStreamFromIndex(int32 index, SoundSEType type) {
AudioIndex *audioIndex = getAudioEntries(type);
AudioEntry audioEntry = {};
if (index < 0 || index >= (int32)(*audioIndex).size())
return nullptr;
audioEntry = (*audioIndex)[index];
// Load patched audio files, if present
if (audioEntry.isPatched && _nameToIndexMISpeechPatched.contains(audioEntry.name)) {
int32 patchedEntry = _nameToIndexMISpeechPatched[audioEntry.name];
type = kSoundSETypePatch;
audioIndex = getAudioEntries(type);
audioEntry = (*audioIndex)[patchedEntry];
}
Common::SeekableReadStream *f = getAudioFile(type);
if (!f)
return nullptr;
Common::SeekableSubReadStream *subStream = new Common::SeekableSubReadStream(
f,
audioEntry.offset,
audioEntry.offset + audioEntry.length,
DisposeAfterUse::YES
);
return createSoundStream(subStream, audioEntry);
}
Common::String calculateCurrentString(const char *msgString) {
char currentChar = *msgString;
bool shouldContinue = true;
char messageBuffer[512];
char *outMsgBuffer = messageBuffer;
memset(messageBuffer, 0, sizeof(messageBuffer));
// 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;
}
int32 SoundSE::handleMISESpeech(const char *msgString, const char *speechFilenameSubstitution,
uint16 roomNumber, uint16 actorTalking, uint16 numWaits) {
// Get the string without the various control codes and special characters...
Common::String currentString = calculateCurrentString(msgString);
const int32 entryIndex = getAppropriateSpeechCue(
currentString.c_str(),
speechFilenameSubstitution,
roomNumber, actorTalking,
(uint16)_currentScriptSavedForSpeechMI,
(uint16)_currentScriptOffsetSavedForSpeechMI,
numWaits
);
if (entryIndex >= 0 && entryIndex < (int32)_audioEntriesMI.size()) {
const AudioEntryMI *entry = &_audioEntriesMI[entryIndex];
//debug("Selected entry: %s (%s)", entry->textEnglish.c_str(), entry->speechFile.c_str());
return _nameToIndexMISpeech.contains(entry->speechFile) ? _nameToIndexMISpeech[entry->speechFile] : -1;
}
return -1;
}
int32 SoundSE::getAmbienceTrack(int32 musicTrack) {
#if 0
// TODO: Read ambience tracks from game resource files
if (musicTrack == 8)
return 18; // SCUMM Bar
else if (musicTrack == 22)
return 2; // Docks
#endif
return -1;
}
void SoundSE::startAmbience(int32 musicTrack) {
int32 ambienceTrack = getAmbienceTrack(musicTrack);
if (ambienceTrack >= 0) {
stopAmbience();
Audio::SeekableAudioStream *ambienceStream = getAudioStreamFromIndex(ambienceTrack, kSoundSETypeAmbience);
if (!ambienceStream)
return;
_mixer->playStream(Audio::Mixer::kMusicSoundType, &_ambienceHandle,
Audio::makeLoopingAudioStream(ambienceStream, 0, 0, 0));
}
}
void SoundSE::stopAmbience() {
_mixer->stopHandle(_ambienceHandle);
}
#if 0
HeaderlessWMAStream::HeaderlessWMAStream(
Common::SeekableReadStream *stream,
AudioEntry entry,
DisposeAfterUse::Flag disposeAfterUse) :
_stream(stream), _entry(entry), _disposeAfterUse(disposeAfterUse) {
// Taken from https://github.com/bgbennyboy/Monkey-Island-Explorer/blob/master/uMIExplorer_XWBManager.pas
const uint16 blockAlignArray[] = {
929, 1487, 1280, 2230, 8917,
8192, 4459, 5945, 2304, 1536,
1485, 1008, 2731, 4096, 6827,
5462, 1280
};
const uint16 index = _entry.align < ARRAYSIZE(blockAlignArray) ? _entry.align : 0;
const uint32 blockAlign = blockAlignArray[index];
const uint32 bitRate = (_entry.bits + 1) * 8;
_wmaCodec = new Audio::WMACodec(2, _entry.rate, _entry.channels, bitRate, blockAlign);
_audioStream = _wmaCodec->decodeFrame(*stream);
}
HeaderlessWMAStream::~HeaderlessWMAStream() {
delete _wmaCodec;
delete _audioStream;
if (_disposeAfterUse == DisposeAfterUse::Flag::YES)
delete _stream;
}
bool HeaderlessWMAStream::seek(const Audio::Timestamp &where) {
if (where == 0) {
return rewind();
}
// Seeking is not supported
return false;
}
int HeaderlessWMAStream::readBuffer(int16 *buffer, const int numSamples) {
int samplesDecoded = 0;
for (;;) {
if (_audioStream) {
samplesDecoded += _audioStream->readBuffer(buffer + samplesDecoded, numSamples - samplesDecoded);
if (_audioStream->endOfData()) {
delete _audioStream;
_audioStream = nullptr;
}
}
if (samplesDecoded == numSamples || endOfData())
break;
if (!_audioStream) {
_audioStream = _wmaCodec->decodeFrame(*_stream);
}
}
return samplesDecoded;
}
#endif
} // End of namespace Scumm