Initial commit
This commit is contained in:
991
engines/scumm/soundse.cpp
Normal file
991
engines/scumm/soundse.cpp
Normal file
@@ -0,0 +1,991 @@
|
||||
/* 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
|
||||
Reference in New Issue
Block a user