diff --git a/engines/sci/engine/kernel.h b/engines/sci/engine/kernel.h index 62566a74b24..d88dc75dab7 100644 --- a/engines/sci/engine/kernel.h +++ b/engines/sci/engine/kernel.h @@ -420,6 +420,26 @@ reg_t kStubNull(EngineState *s, int argc, reg_t *argv); #ifdef ENABLE_SCI32 // SCI2 Kernel Functions +reg_t kDoAudio32(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioInit(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioWaitForPlay(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioPlay(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioStop(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioPause(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioResume(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioPosition(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioRate(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioVolume(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioGetCapability(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioBitDepth(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioDistort(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioMixing(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioChannels(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioPreload(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioFade(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioHasSignal(EngineState *s, int argc, reg_t *argv); +reg_t kDoAudioSetLoop(EngineState *s, int argc, reg_t *argv); + reg_t kIsHiRes(EngineState *s, int argc, reg_t *argv); reg_t kArray(EngineState *s, int argc, reg_t *argv); reg_t kListAt(EngineState *s, int argc, reg_t *argv); diff --git a/engines/sci/engine/kernel_tables.h b/engines/sci/engine/kernel_tables.h index 0ede307e6b3..a61213edb7d 100644 --- a/engines/sci/engine/kernel_tables.h +++ b/engines/sci/engine/kernel_tables.h @@ -168,7 +168,7 @@ static const SciKernelMapSubEntry kDoSound_subops[] = { // signature for SCI21 should be "o" { SIG_SOUNDSCI21, 9, MAP_CALL(DoSoundStop), NULL, NULL }, { SIG_SOUNDSCI21, 10, MAP_CALL(DoSoundPause), NULL, NULL }, - { SIG_SOUNDSCI21, 11, MAP_CALL(DoSoundFade), NULL, kDoSoundFade_workarounds }, + { SIG_SOUNDSCI21, 11, MAP_CALL(DoSoundFade), "oiiii", kDoSoundFade_workarounds }, { SIG_SOUNDSCI21, 12, MAP_CALL(DoSoundSetHold), NULL, NULL }, { SIG_SOUNDSCI21, 13, MAP_CALL(DoSoundDummy), NULL, NULL }, { SIG_SOUNDSCI21, 14, MAP_CALL(DoSoundSetVolume), NULL, NULL }, @@ -182,6 +182,67 @@ static const SciKernelMapSubEntry kDoSound_subops[] = { SCI_SUBOPENTRY_TERMINATOR }; +#ifdef ENABLE_SCI32 +// NOTE: In SSCI, some 'unused' kDoAudio subops are actually +// called indirectly by kDoSound: +// +// kDoSoundGetAudioCapability -> kDoAudioGetCapability +// kDoSoundPlay -> kDoAudioPlay, kDoAudioStop +// kDoSoundPause -> kDoAudioPause, kDoAudioResume +// kDoSoundFade -> kDoAudioFade +// kDoSoundSetVolume -> kDoAudioVolume +// kDoSoundSetLoop -> kDoAudioSetLoop +// kDoSoundUpdateCues -> kDoAudioPosition +// +// In ScummVM, logic inside these kernel functions has been +// moved to methods of Audio32, and direct calls to Audio32 +// are made from kDoSound instead. +// +// Some kDoAudio methods are esoteric and appear to be used +// only by one or two games: +// +// kDoAudioMixing: Phantasmagoria (other games call this +// function, but only to disable the feature) +// kDoAudioHasSignal: SQ6 TalkRandCycle +// kDoAudioPan: Rama RegionSFX::pan method +// +// Finally, there is a split in SCI2.1mid audio code. +// QFG4CD & SQ6 do not have opcodes 18 and 19, but they +// exist in GK2, KQ7 2.00b, Phantasmagoria 1, PQ:SWAT, and +// Torin. (It is unknown if they exist in MUMG Deluxe or +// Shivers 1; they are not used in either of these games.) + +// version, subId, function-mapping, signature, workarounds +static const SciKernelMapSubEntry kDoAudio_subops[] = { + { SIG_SCI32, 0, MAP_CALL(DoAudioInit), "", NULL }, + // SCI2 includes a Sync script that would call + // kDoAudioWaitForPlay, but SSCI has no opcode 1 until + // SCI2.1early + { SIG_SINCE_SCI21, 1, MAP_CALL(DoAudioWaitForPlay), "(i)(i)(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 2, MAP_CALL(DoAudioPlay), "(i)(i)(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 3, MAP_CALL(DoAudioStop), "(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 4, MAP_CALL(DoAudioPause), "(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 5, MAP_CALL(DoAudioResume), "(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 6, MAP_CALL(DoAudioPosition), "(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 7, MAP_CALL(DoAudioRate), "(i)", NULL }, + { SIG_SCI32, 8, MAP_CALL(DoAudioVolume), "(i)(i)(i)(i)(i)(i)", NULL }, + { SIG_SCI32, 9, MAP_CALL(DoAudioGetCapability), "", NULL }, + { SIG_SCI32, 10, MAP_CALL(DoAudioBitDepth), "(i)", NULL }, + { SIG_SCI32, 11, MAP_DUMMY(DoAudioDistort), "(i)", NULL }, + { SIG_SCI32, 12, MAP_CALL(DoAudioMixing), "(i)", NULL }, + { SIG_SCI32, 13, MAP_CALL(DoAudioChannels), "(i)", NULL }, + { SIG_SCI32, 14, MAP_CALL(DoAudioPreload), "(i)", NULL }, + { SIG_SINCE_SCI21MID, 15, MAP_CALL(DoAudioFade), "(iiii)(i)(i)", NULL }, + { SIG_SINCE_SCI21MID, 16, MAP_DUMMY(DoAudioFade36), "iiiii(iii)(i)", NULL }, + { SIG_SINCE_SCI21MID, 17, MAP_CALL(DoAudioHasSignal), "", NULL }, + { SIG_SINCE_SCI21MID, 18, MAP_EMPTY(DoAudioCritical), "", NULL }, + { SIG_SINCE_SCI21MID, 19, MAP_CALL(DoAudioSetLoop), "iii(o)", NULL }, + { SIG_SCI3, 20, MAP_DUMMY(DoAudioPan), "", NULL }, + { SIG_SCI3, 21, MAP_DUMMY(DoAudioPanOff), "", NULL }, + SCI_SUBOPENTRY_TERMINATOR +}; +#endif + // version, subId, function-mapping, signature, workarounds static const SciKernelMapSubEntry kGraph_subops[] = { { SIG_SCI32, 1, MAP_CALL(StubNull), "", NULL }, // called by gk1 sci32 right at the start @@ -484,7 +545,10 @@ static SciKernelMapEntry s_kernelMap[] = { { MAP_CALL(DisposeList), SIG_EVERYWHERE, "l", NULL, NULL }, { MAP_CALL(DisposeScript), SIG_EVERYWHERE, "i(i*)", NULL, kDisposeScript_workarounds }, { MAP_CALL(DisposeWindow), SIG_EVERYWHERE, "i(i)", NULL, NULL }, - { MAP_CALL(DoAudio), SIG_EVERYWHERE, "i(.*)", NULL, NULL }, // subop + { MAP_CALL(DoAudio), SCI_VERSION_NONE, SCI_VERSION_2, SIGFOR_ALL, "i(.*)", NULL, NULL }, // subop +#ifdef ENABLE_SCI32 + { "DoAudio", kDoAudio32, SIG_SINCE_SCI21, SIGFOR_ALL, "(.*)", kDoAudio_subops, NULL }, +#endif { MAP_CALL(DoAvoider), SIG_EVERYWHERE, "o(i)", NULL, NULL }, { MAP_CALL(DoBresen), SIG_EVERYWHERE, "o", NULL, NULL }, { MAP_CALL(DoSound), SIG_EVERYWHERE, "i(.*)", kDoSound_subops, NULL }, diff --git a/engines/sci/engine/ksound.cpp b/engines/sci/engine/ksound.cpp index 2b22d6867b8..301e4c1b08e 100644 --- a/engines/sci/engine/ksound.cpp +++ b/engines/sci/engine/ksound.cpp @@ -26,6 +26,9 @@ #include "sci/engine/kernel.h" #include "sci/engine/vm.h" // for Object #include "sci/sound/audio.h" +#ifdef ENABLE_SCI32 +#include "sci/sound/audio32.h" +#endif #include "sci/sound/soundcmd.h" #include "sci/sound/sync.h" @@ -114,7 +117,8 @@ reg_t kDoCdAudio(EngineState *s, int argc, reg_t *argv) { } /** - * Used for speech playback and digital soundtracks in CD games + * Used for speech playback and digital soundtracks in CD games. + * This is the SCI16 version; SCI32 is handled separately. */ reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) { // JonesCD uses different functions based on the cdaudio.map file @@ -185,14 +189,6 @@ reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) { int16 volume = argv[1].toUint16(); volume = CLIP(volume, 0, AUDIO_VOLUME_MAX); debugC(kDebugLevelSound, "kDoAudio: set volume to %d", volume); -#ifdef ENABLE_SCI32 - if (getSciVersion() >= SCI_VERSION_2_1_EARLY) { - int16 volumePrev = mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType) / 2; - volumePrev = CLIP(volumePrev, 0, AUDIO_VOLUME_MAX); - mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, volume * 2); - return make_reg(0, volumePrev); - } else -#endif mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, volume * 2); break; } @@ -233,12 +229,6 @@ reg_t kDoAudio(EngineState *s, int argc, reg_t *argv) { if (getSciVersion() <= SCI_VERSION_1_1) { debugC(kDebugLevelSound, "kDoAudio: CD audio subop"); return kDoCdAudio(s, argc - 1, argv + 1); -#ifdef ENABLE_SCI32 - } else { - // TODO: This isn't CD Audio in SCI32 anymore - warning("kDoAudio: Unhandled case 10, %d extra arguments passed", argc - 1); - break; -#endif } // 3 new subops in Pharkas CD (including CD demo). kDoAudio in Pharkas sits at seg026:038C @@ -320,6 +310,155 @@ reg_t kDoSync(EngineState *s, int argc, reg_t *argv) { } #ifdef ENABLE_SCI32 +reg_t kDoAudio32(EngineState *s, int argc, reg_t *argv) { + if (!s) + return make_reg(0, getSciVersion()); + error("not supposed to call this"); +} + +reg_t kDoAudioInit(EngineState *s, int argc, reg_t *argv) { + return make_reg(0, 0); +} + +reg_t kDoAudioWaitForPlay(EngineState *s, int argc, reg_t *argv) { + return g_sci->_audio32->kernelPlay(false, argc, argv); +} + +reg_t kDoAudioPlay(EngineState *s, int argc, reg_t *argv) { + return g_sci->_audio32->kernelPlay(true, argc, argv); +} + +reg_t kDoAudioStop(EngineState *s, int argc, reg_t *argv) { + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG); + return make_reg(0, g_sci->_audio32->stop(channelIndex)); +} + +reg_t kDoAudioPause(EngineState *s, int argc, reg_t *argv) { + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG); + return make_reg(0, g_sci->_audio32->pause(channelIndex)); +} + +reg_t kDoAudioResume(EngineState *s, int argc, reg_t *argv) { + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG); + return make_reg(0, g_sci->_audio32->resume(channelIndex)); +} + +reg_t kDoAudioPosition(EngineState *s, int argc, reg_t *argv) { + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc > 1 ? argv[1] : NULL_REG); + return make_reg(0, g_sci->_audio32->getPosition(channelIndex)); +} + +reg_t kDoAudioRate(EngineState *s, int argc, reg_t *argv) { + // NOTE: In the original engine this would set the hardware + // DSP sampling rate; ScummVM mixer does not need this, so + // we only store the value to satisfy engine compatibility. + + if (argc > 0) { + const uint16 sampleRate = argv[0].toUint16(); + if (sampleRate != 0) { + g_sci->_audio32->setSampleRate(sampleRate); + } + } + + return make_reg(0, g_sci->_audio32->getSampleRate()); +} + +reg_t kDoAudioVolume(EngineState *s, int argc, reg_t *argv) { + const int16 volume = argc > 0 ? argv[0].toSint16() : -1; + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 1, argc > 2 ? argv[2] : NULL_REG); + + if (volume != -1) { + g_sci->_audio32->setVolume(channelIndex, volume); + } + + return make_reg(0, g_sci->_audio32->getVolume(channelIndex)); +} + +reg_t kDoAudioGetCapability(EngineState *s, int argc, reg_t *argv) { + return make_reg(0, 1); +} + +reg_t kDoAudioBitDepth(EngineState *s, int argc, reg_t *argv) { + // NOTE: In the original engine this would set the hardware + // DSP bit depth; ScummVM mixer does not need this, so + // we only store the value to satisfy engine compatibility. + + if (argc > 0) { + const uint16 bitDepth = argv[0].toUint16(); + if (bitDepth != 0) { + g_sci->_audio32->setBitDepth(bitDepth); + } + } + + return make_reg(0, g_sci->_audio32->getBitDepth()); +} + +reg_t kDoAudioMixing(EngineState *s, int argc, reg_t *argv) { + if (argc > 0) { + g_sci->_audio32->setAttenuatedMixing(argv[0].toUint16()); + } + + return make_reg(0, g_sci->_audio32->getAttenuatedMixing()); +} + +reg_t kDoAudioChannels(EngineState *s, int argc, reg_t *argv) { + // NOTE: In the original engine this would set the hardware + // DSP stereo output; ScummVM mixer does not need this, so + // we only store the value to satisfy engine compatibility. + + if (argc > 0) { + const int16 numChannels = argv[0].toSint16(); + if (numChannels != 0) { + g_sci->_audio32->setNumOutputChannels(numChannels); + } + } + + return make_reg(0, g_sci->_audio32->getNumOutputChannels()); +} + +reg_t kDoAudioPreload(EngineState *s, int argc, reg_t *argv) { + // NOTE: In the original engine this would cause audio + // data for new channels to be preloaded to memory when + // the channel was initialized; we do not need this, so + // we only store the value to satisfy engine compatibility. + + if (argc > 0) { + g_sci->_audio32->setPreload(argv[0].toUint16()); + } + + return make_reg(0, g_sci->_audio32->getPreload()); +} + +reg_t kDoAudioFade(EngineState *s, int argc, reg_t *argv) { + if (argc < 4) { + return make_reg(0, 0); + } + + // NOTE: Sierra did a nightmarish hack here, temporarily replacing + // the argc of the kernel arguments with 2 and then restoring it + // after findChannelByArgs was called. + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(2, argv, 0, argc > 5 ? argv[5] : NULL_REG); + + const int16 volume = argv[1].toSint16(); + const int16 speed = argv[2].toSint16(); + const int16 steps = argv[3].toSint16(); + const bool stopAfterFade = argc > 4 ? (bool)argv[4].toUint16() : false; + + return make_reg(0, g_sci->_audio32->fadeChannel(channelIndex, volume, speed, steps, stopAfterFade)); +} + +reg_t kDoAudioHasSignal(EngineState *s, int argc, reg_t *argv) { + return make_reg(0, g_sci->_audio32->hasSignal()); +} + +reg_t kDoAudioSetLoop(EngineState *s, int argc, reg_t *argv) { + const int16 channelIndex = g_sci->_audio32->findChannelByArgs(argc, argv, 0, argc == 3 ? argv[2] : NULL_REG); + + const bool loop = argv[0].toSint16() != 0 && argv[0].toSint16() != 1; + + g_sci->_audio32->setLoop(channelIndex, loop); + return s->r_acc; +} reg_t kSetLanguage(EngineState *s, int argc, reg_t *argv) { // This is used by script 90 of MUMG Deluxe from the main menu to toggle diff --git a/engines/sci/module.mk b/engines/sci/module.mk index c35d0b51ddd..69ddf001834 100644 --- a/engines/sci/module.mk +++ b/engines/sci/module.mk @@ -91,6 +91,8 @@ MODULE_OBJS += \ graphics/palette32.o \ graphics/screen_item32.o \ graphics/text32.o \ + sound/audio32.o \ + sound/decoders/sol.o \ video/robot_decoder.o endif diff --git a/engines/sci/resource.cpp b/engines/sci/resource.cpp index 9f977aaefdf..2a83a572270 100644 --- a/engines/sci/resource.cpp +++ b/engines/sci/resource.cpp @@ -26,6 +26,9 @@ #include "common/fs.h" #include "common/macresman.h" #include "common/textconsole.h" +#ifdef ENABLE_SCI32 +#include "common/memstream.h" +#endif #include "sci/resource.h" #include "sci/resource_intern.h" @@ -221,6 +224,12 @@ void Resource::writeToStream(Common::WriteStream *stream) const { stream->write(data, size); } +#ifdef ENABLE_SCI32 +Common::SeekableReadStream *Resource::makeStream() const { + return new Common::MemoryReadStream(data, size, DisposeAfterUse::NO); +} +#endif + uint32 Resource::getAudioCompressionType() const { return _source->getAudioCompressionType(); } @@ -229,7 +238,6 @@ uint32 AudioVolumeResourceSource::getAudioCompressionType() const { return _audioCompressionType; } - ResourceSource::ResourceSource(ResSourceType type, const Common::String &name, int volNum, const Common::FSNode *resFile) : _sourceType(type), _name(name), _volumeNumber(volNum), _resourceFile(resFile) { _scanned = false; @@ -1445,13 +1453,18 @@ void ResourceManager::readResourcePatchesBase36() { files.clear(); // audio36 resources start with a @, A, or B - // sync36 resources start with a # + // sync36 resources start with a #, S, or T if (i == kResourceTypeAudio36) { SearchMan.listMatchingMembers(files, "@???????.???"); SearchMan.listMatchingMembers(files, "A???????.???"); SearchMan.listMatchingMembers(files, "B???????.???"); - } else + } else { SearchMan.listMatchingMembers(files, "#???????.???"); +#ifdef ENABLE_SCI32 + SearchMan.listMatchingMembers(files, "S???????.???"); + SearchMan.listMatchingMembers(files, "T???????.???"); +#endif + } for (Common::ArchiveMemberList::const_iterator x = files.begin(); x != files.end(); ++x) { name = (*x)->getName(); diff --git a/engines/sci/resource.h b/engines/sci/resource.h index ef474d97c2f..f70bf48bd44 100644 --- a/engines/sci/resource.h +++ b/engines/sci/resource.h @@ -84,7 +84,10 @@ enum ResourceType { kResourceTypePatch, kResourceTypeBitmap, kResourceTypePalette, - kResourceTypeCdAudio, + kResourceTypeCdAudio = 12, +#ifdef ENABLE_SCI32 + kResourceTypeWave = 12, +#endif kResourceTypeAudio, kResourceTypeSync, kResourceTypeMessage, @@ -212,6 +215,10 @@ public: return (_type == other._type) && (_number == other._number) && (_tuple == other._tuple); } + bool operator!=(const ResourceId &other) const { + return !operator==(other); + } + bool operator<(const ResourceId &other) const { return (_type < other._type) || ((_type == other._type) && (_number < other._number)) || ((_type == other._type) && (_number == other._number) && (_tuple < other._tuple)); @@ -259,6 +266,10 @@ public: */ void writeToStream(Common::WriteStream *stream) const; +#ifdef ENABLE_SCI32 + Common::SeekableReadStream *makeStream() const; +#endif + const Common::String &getResourceLocation() const; // FIXME: This audio specific method is a hack. After all, why should a diff --git a/engines/sci/resource_audio.cpp b/engines/sci/resource_audio.cpp index 5717a091211..5aeff81b77f 100644 --- a/engines/sci/resource_audio.cpp +++ b/engines/sci/resource_audio.cpp @@ -25,7 +25,7 @@ #include "common/archive.h" #include "common/file.h" #include "common/textconsole.h" - +#include "common/memstream.h" #include "sci/resource.h" #include "sci/resource_intern.h" #include "sci/util.h" diff --git a/engines/sci/sci.cpp b/engines/sci/sci.cpp index 8c23d322e75..6244c43764e 100644 --- a/engines/sci/sci.cpp +++ b/engines/sci/sci.cpp @@ -68,6 +68,7 @@ #include "sci/graphics/palette32.h" #include "sci/graphics/text32.h" #include "sci/graphics/frameout.h" +#include "sci/sound/audio32.h" #include "sci/video/robot_decoder.h" #endif @@ -88,6 +89,9 @@ SciEngine::SciEngine(OSystem *syst, const ADGameDescription *desc, SciGameId gam _audio = 0; _sync = nullptr; +#ifdef ENABLE_SCI32 + _audio32 = nullptr; +#endif _features = 0; _resMan = 0; _gamestate = 0; @@ -167,6 +171,7 @@ SciEngine::~SciEngine() { delete _robotDecoder; delete _gfxFrameout; delete _gfxRemap32; + delete _audio32; #endif delete _gfxMenu; delete _gfxControls16; @@ -269,7 +274,13 @@ Common::Error SciEngine::run() { // Also, XMAS1990 apparently had a parser too. Refer to http://forums.scummvm.org/viewtopic.php?t=9135 if (getGameId() == GID_CHRISTMAS1990) _vocabulary = new Vocabulary(_resMan, false); - _audio = new AudioPlayer(_resMan); + +#ifdef ENABLE_SCI32 + if (getSciVersion() >= SCI_VERSION_2_1_EARLY) { + _audio32 = new Audio32(_resMan); + } else +#endif + _audio = new AudioPlayer(_resMan); _sync = new Sync(_resMan, segMan); _gamestate = new EngineState(segMan); _eventMan = new EventManager(_resMan->detectFontExtended()); @@ -805,7 +816,9 @@ void SciEngine::runGame() { void SciEngine::exitGame() { if (_gamestate->abortScriptProcessing != kAbortLoadGame) { _gamestate->_executionStack.clear(); - _audio->stopAllAudio(); + if (_audio) { + _audio->stopAllAudio(); + } _sync->stop(); _soundCmd->clearPlayList(); } diff --git a/engines/sci/sci.h b/engines/sci/sci.h index ff414727fa7..56ee2f4403b 100644 --- a/engines/sci/sci.h +++ b/engines/sci/sci.h @@ -83,6 +83,7 @@ class GfxTransitions; #ifdef ENABLE_SCI32 class RobotDecoder; class GfxFrameout; +class Audio32; #endif // our engine debug levels @@ -368,6 +369,7 @@ public: GfxMacIconBar *_gfxMacIconBar; // Mac Icon Bar manager #ifdef ENABLE_SCI32 + Audio32 *_audio32; RobotDecoder *_robotDecoder; GfxFrameout *_gfxFrameout; // kFrameout and the like for 32-bit gfx #endif diff --git a/engines/sci/sound/audio.cpp b/engines/sci/sound/audio.cpp index 71b081049e0..4fb9a580035 100644 --- a/engines/sci/sound/audio.cpp +++ b/engines/sci/sound/audio.cpp @@ -253,13 +253,7 @@ static void deDPCM16(byte *soundBuf, Common::SeekableReadStream &audioStream, ui static void deDPCM8Nibble(byte *soundBuf, int32 &s, byte b) { if (b & 8) { -#ifdef ENABLE_SCI32 - // SCI2.1 reverses the order of the table values here - if (getSciVersion() >= SCI_VERSION_2_1_EARLY) - s -= tableDPCM8[b & 7]; - else -#endif - s -= tableDPCM8[7 - (b & 7)]; + s -= tableDPCM8[7 - (b & 7)]; } else s += tableDPCM8[b & 7]; s = CLIP(s, 0, 255); diff --git a/engines/sci/sound/audio32.cpp b/engines/sci/sound/audio32.cpp new file mode 100644 index 00000000000..56b10c55fe9 --- /dev/null +++ b/engines/sci/sound/audio32.cpp @@ -0,0 +1,961 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "sci/sound/audio32.h" +#include "audio/audiostream.h" // for SeekableAudioStream +#include "audio/decoders/raw.h" // for makeRawStream, RawFlags::FLAG_16BITS +#include "audio/decoders/wave.h" // for makeWAVStream +#include "audio/rate.h" // for RateConverter, makeRateConverter +#include "audio/timestamp.h" // for Timestamp +#include "common/config-manager.h" // for ConfMan +#include "common/endian.h" // for MKTAG +#include "common/memstream.h" // for MemoryReadStream +#include "common/str.h" // for String +#include "common/stream.h" // for SeekableReadStream +#include "common/system.h" // for OSystem, g_system +#include "common/textconsole.h" // for warning +#include "common/types.h" // for Flag::NO +#include "engine.h" // for Engine, g_engine +#include "sci/engine/vm_types.h" // for reg_t, make_reg, NULL_REG +#include "sci/resource.h" // for ResourceId, ResourceType::kResour... +#include "sci/sci.h" // for SciEngine, g_sci, getSciVersion +#include "sci/sound/decoders/sol.h" // for makeSOLStream + +namespace Sci { + +bool detectSolAudio(Common::SeekableReadStream &stream) { + const size_t initialPosition = stream.pos(); + +// TODO: Resource manager for audio resources reads past the +// header so even though this is the detection algorithm +// in SSCI, ScummVM can't use it +#if 0 + byte header[6]; + if (stream.read(header, sizeof(header)) != sizeof(header)) { + stream.seek(initialPosition); + return false; + } + + stream.seek(initialPosition); + + if (header[0] != 0x8d || READ_BE_UINT32(header + 2) != MKTAG('S', 'O', 'L', 0)) { + return false; + } + + return true; +#else + byte header[4]; + if (stream.read(header, sizeof(header)) != sizeof(header)) { + stream.seek(initialPosition); + return false; + } + + stream.seek(initialPosition); + + if (READ_BE_UINT32(header) != MKTAG('S', 'O', 'L', 0)) { + return false; + } + + return true; +#endif +} + +bool detectWaveAudio(Common::SeekableReadStream &stream) { + const size_t initialPosition = stream.pos(); + + byte blockHeader[8]; + if (stream.read(blockHeader, sizeof(blockHeader)) != sizeof(blockHeader)) { + stream.seek(initialPosition); + return false; + } + + stream.seek(initialPosition); + const uint32 headerType = READ_BE_UINT32(blockHeader); + + if (headerType != MKTAG('R', 'I', 'F', 'F')) { + return false; + } + + return true; +} + +#pragma mark - + +Audio32::Audio32(ResourceManager *resMan) : + _resMan(resMan), + _mixer(g_system->getMixer()), + _handle(), + _mutex(), + + _numActiveChannels(0), + + _maxAllowedSampleRate(44100), + _maxAllowedBitDepth(16), + _maxAllowedOutputChannels(2), + _preload(0), + + _robotAudioPaused(false), + + _pausedAtTick(0), + _startedAtTick(0), + + _attenuatedMixing(true), + + _monitoredChannelIndex(-1), + _monitoredBuffer(nullptr), + _monitoredBufferSize(0), + _numMonitoredSamples(0) { + + if (getSciVersion() < SCI_VERSION_3) { + _channels.resize(5); + } else { + _channels.resize(8); + } + + _useModifiedAttenuation = false; + if (getSciVersion() == SCI_VERSION_2_1_MIDDLE) { + switch (g_sci->getGameId()) { + case GID_MOTHERGOOSEHIRES: + case GID_PQ4: + case GID_QFG4: + case GID_SQ6: + _useModifiedAttenuation = true; + default: + break; + } + } else if (getSciVersion() == SCI_VERSION_2_1_EARLY) { + switch (g_sci->getGameId()) { + // 1.51 uses the non-standard attenuation, but 2.00b + // does not, which is strange. + case GID_KQ7: + _useModifiedAttenuation = true; + default: + break; + } + } + + _mixer->playStream(Audio::Mixer::kSFXSoundType, &_handle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true); + _mixer->pauseHandle(_handle, true); +} + +Audio32::~Audio32() { + stop(kAllChannels); + _mixer->stopHandle(_handle); + free(_monitoredBuffer); +} + +#pragma mark - +#pragma mark AudioStream implementation + +int Audio32::writeAudioInternal(Audio::RewindableAudioStream *const sourceStream, Audio::RateConverter *const converter, Audio::st_sample_t *targetBuffer, const int numSamples, const Audio::st_volume_t leftVolume, const Audio::st_volume_t rightVolume, const bool loop) { + int samplesToRead = numSamples; + + // The parent rate converter will request N * 2 + // samples from this `readBuffer` call, because + // we tell it that we send stereo output, but + // the source stream we're mixing in may be + // mono, in which case we need to request half + // as many samples from the mono stream and let + // the converter double them for stereo output + if (!sourceStream->isStereo()) { + samplesToRead >>= 1; + } + + int samplesWritten = 0; + + do { + if (loop && sourceStream->endOfStream()) { + sourceStream->rewind(); + } + + const int loopSamplesWritten = converter->flow(*sourceStream, targetBuffer, samplesToRead, leftVolume, rightVolume); + + if (loopSamplesWritten == 0) { + break; + } + + samplesToRead -= loopSamplesWritten; + samplesWritten += loopSamplesWritten; + targetBuffer += loopSamplesWritten << 1; + } while (loop && samplesToRead > 0); + + if (!sourceStream->isStereo()) { + samplesWritten <<= 1; + } + + return samplesWritten; +} + +// In earlier versions of SCI32 engine, audio mixing is +// split into three different functions. +// +// The first function is called from the main game thread in +// AsyncEventCheck; later versions of SSCI also call it when +// getting the playback position. This function is +// responsible for cleaning up finished channels and +// filling active channel buffers with decompressed audio +// matching the hardware output audio format so they can +// just be copied into the main DAC buffer directly later. +// +// The second function is called by the audio hardware when +// the DAC buffer needs to be filled, and by `play` when +// there is only one active sample (so it can just blow away +// whatever was already in the DAC buffer). It merges all +// active channels into the DAC buffer and then updates the +// offset into the DAC buffer. +// +// Finally, a third function is called by the second +// function, and it actually puts data into the DAC buffer, +// performing volume, distortion, and balance adjustments. +// +// Since we only have one callback from the audio thread, +// and should be able to do all audio processing in +// real time, and we have streams, and we do not need to +// completely fill the audio buffer, the functionality of +// all these original functions is combined here and +// simplified. +int Audio32::readBuffer(Audio::st_sample_t *const buffer, const int numSamples) { + Common::StackLock lock(_mutex); + + // The system mixer should not try to get data when + // Audio32 is paused + assert(_pausedAtTick == 0 && _numActiveChannels > 0); + + // The caller of `readBuffer` is a rate converter, + // which reuses (without clearing) an intermediate + // buffer, so we need to zero the intermediate buffer + // to prevent mixing into audio data from the last + // callback. + memset(buffer, 0, numSamples * sizeof(Audio::st_sample_t)); + + // This emulates the attenuated mixing mode of SSCI + // engine, which reduces the volume of the target + // buffer when each new channel is mixed in. + // Instead of manipulating the content of the target + // buffer when mixing (which would either require + // modification of RateConverter or an expensive second + // pass against the entire target buffer), we just + // scale the volume for each channel in advance, with + // the earliest (lowest) channel having the highest + // amount of attenuation (lowest volume). + uint8 attenuationAmount; + uint8 attenuationStepAmount; + if (_useModifiedAttenuation) { + // channel | divisor + // 0 | 0 (>> 0) + // 1 | 4 (>> 2) + // 2 | 8... + attenuationAmount = _numActiveChannels * 2; + attenuationStepAmount = 2; + } else { + // channel | divisor + // 0 | 2 (>> 1) + // 1 | 4 (>> 2) + // 2 | 6... + if (_monitoredChannelIndex == -1 && _numActiveChannels > 1) { + attenuationAmount = _numActiveChannels + 1; + attenuationStepAmount = 1; + } else { + attenuationAmount = 0; + attenuationStepAmount = 0; + } + } + + int maxSamplesWritten = 0; + + for (int16 channelIndex = 0; channelIndex < _numActiveChannels; ++channelIndex) { + attenuationAmount -= attenuationStepAmount; + + const AudioChannel &channel = getChannel(channelIndex); + + if (channel.pausedAtTick || (channel.robot && _robotAudioPaused)) { + continue; + } + + // Channel finished fading and had the + // stopChannelOnFade flag set, so no longer exists + if (channel.fadeStepsRemaining && processFade(channelIndex)) { + --channelIndex; + continue; + } + + if (channel.robot) { + // TODO: Robot audio into output buffer + continue; + } + + if (channel.vmd) { + // TODO: VMD audio into output buffer + continue; + } + + Audio::st_volume_t leftVolume, rightVolume; + + if (channel.pan == -1 || !isStereo()) { + leftVolume = rightVolume = channel.volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume; + } else { + // TODO: This should match the SCI3 algorithm, + // which seems to halve the volume of each + // channel when centered; is this intended? + leftVolume = channel.volume * (100 - channel.pan) / 100 * Audio::Mixer::kMaxChannelVolume / kMaxVolume; + rightVolume = channel.volume * channel.pan / 100 * Audio::Mixer::kMaxChannelVolume / kMaxVolume; + } + + if (_monitoredChannelIndex == -1 && _attenuatedMixing) { + leftVolume >>= attenuationAmount; + rightVolume >>= attenuationAmount; + } + + if (channelIndex == _monitoredChannelIndex) { + const size_t bufferSize = numSamples * sizeof(Audio::st_sample_t); + if (_monitoredBufferSize < bufferSize) { + _monitoredBuffer = (Audio::st_sample_t *)realloc(_monitoredBuffer, bufferSize); + _monitoredBufferSize = bufferSize; + } + + memset(_monitoredBuffer, 0, _monitoredBufferSize); + + _numMonitoredSamples = writeAudioInternal(channel.stream, channel.converter, _monitoredBuffer, numSamples, leftVolume, rightVolume, channel.loop); + + Audio::st_sample_t *sourceBuffer = _monitoredBuffer; + Audio::st_sample_t *targetBuffer = buffer; + const Audio::st_sample_t *const end = _monitoredBuffer + _numMonitoredSamples; + while (sourceBuffer != end) { + Audio::clampedAdd(*targetBuffer++, *sourceBuffer++); + } + + if (_numMonitoredSamples > maxSamplesWritten) { + maxSamplesWritten = _numMonitoredSamples; + } + } else if (!channel.stream->endOfStream() || channel.loop) { + const int channelSamplesWritten = writeAudioInternal(channel.stream, channel.converter, buffer, numSamples, leftVolume, rightVolume, channel.loop); + if (channelSamplesWritten > maxSamplesWritten) { + maxSamplesWritten = channelSamplesWritten; + } + } + } + + freeUnusedChannels(); + + return maxSamplesWritten; +} + +#pragma mark - +#pragma mark Channel management + +int16 Audio32::findChannelByArgs(int argc, const reg_t *argv, const int startIndex, const reg_t soundNode) const { + // NOTE: argc/argv are already reduced by one in our engine because + // this call is always made from a subop, so no reduction for the + // subop is made in this function. SSCI takes extra steps to skip + // the subop argument. + + argc -= startIndex; + if (argc <= 0) { + return kAllChannels; + } + + Common::StackLock lock(_mutex); + + if (_numActiveChannels == 0) { + return kNoExistingChannel; + } + + ResourceId searchId; + + if (argc < 5) { + searchId = ResourceId(kResourceTypeAudio, argv[startIndex].toUint16()); + } else { + searchId = ResourceId( + kResourceTypeAudio36, + argv[startIndex].toUint16(), + argv[startIndex + 1].toUint16(), + argv[startIndex + 2].toUint16(), + argv[startIndex + 3].toUint16(), + argv[startIndex + 4].toUint16() + ); + } + + return findChannelById(searchId, soundNode); +} + +int16 Audio32::findChannelById(const ResourceId resourceId, const reg_t soundNode) const { + Common::StackLock lock(_mutex); + + if (_numActiveChannels == 0) { + return kNoExistingChannel; + } + + if (resourceId.getType() == kResourceTypeAudio) { + for (int16 i = 0; i < _numActiveChannels; ++i) { + const AudioChannel channel = _channels[i]; + if ( + channel.id == resourceId && + (soundNode.isNull() || soundNode == channel.soundNode) + ) { + return i; + } + } + } else if (resourceId.getType() == kResourceTypeAudio36) { + for (int16 i = 0; i < _numActiveChannels; ++i) { + const AudioChannel &candidate = getChannel(i); + if (!candidate.robot && candidate.id == resourceId) { + return i; + } + } + } else { + error("Audio32::findChannelById: Unknown resource type %d", resourceId.getType()); + } + + return kNoExistingChannel; +} + +void Audio32::freeUnusedChannels() { + Common::StackLock lock(_mutex); + for (int channelIndex = 0; channelIndex < _numActiveChannels; ++channelIndex) { + const AudioChannel &channel = getChannel(channelIndex); + if (channel.stream->endOfStream()) { + if (channel.loop) { + channel.stream->rewind(); + } else { + stop(channelIndex--); + } + } + } +} + +void Audio32::freeChannel(const int16 channelIndex) { + // The original engine did this: + // 1. Unlock memory-cached resource, if one existed + // 2. Close patched audio file descriptor, if one existed + // 3. Free decompression memory buffer, if one existed + // 4. Clear monitored memory buffer, if one existed + Common::StackLock lock(_mutex); + AudioChannel &channel = getChannel(channelIndex); + _resMan->unlockResource(channel.resource); + channel.resource = nullptr; + delete channel.stream; + channel.stream = nullptr; + delete channel.resourceStream; + channel.resourceStream = nullptr; + delete channel.converter; + channel.converter = nullptr; + + if (_monitoredChannelIndex == channelIndex) { + _monitoredChannelIndex = -1; + } +} + +#pragma mark - +#pragma mark Script compatibility + +void Audio32::setSampleRate(uint16 rate) { + if (rate > _maxAllowedSampleRate) { + rate = _maxAllowedSampleRate; + } + + _globalSampleRate = rate; +} + +void Audio32::setBitDepth(uint8 depth) { + if (depth > _maxAllowedBitDepth) { + depth = _maxAllowedBitDepth; + } + + _globalBitDepth = depth; +} + +void Audio32::setNumOutputChannels(int16 numChannels) { + if (numChannels > _maxAllowedOutputChannels) { + numChannels = _maxAllowedOutputChannels; + } + + _globalNumOutputChannels = numChannels; +} + +#pragma mark - +#pragma mark Playback + +uint16 Audio32::play(int16 channelIndex, const ResourceId resourceId, const bool autoPlay, const bool loop, const int16 volume, const reg_t soundNode, const bool monitor) { + Common::StackLock lock(_mutex); + + if (channelIndex != kNoExistingChannel) { + AudioChannel &channel = getChannel(channelIndex); + + if (channel.pausedAtTick) { + resume(channelIndex); + return MIN(65534, 1 + channel.stream->getLength().msecs() * 60 / 1000); + } + + warning("Tried to resume channel %s that was not paused", channel.id.toString().c_str()); + return MIN(65534, 1 + channel.stream->getLength().msecs() * 60 / 1000); + } + + if (_numActiveChannels == _channels.size()) { + warning("Audio mixer is full when trying to play %s", resourceId.toString().c_str()); + return 0; + } + + // NOTE: SCI engine itself normally searches in this order: + // + // For Audio36: + // + // 1. First, request a FD using Audio36 name and use it as the + // source FD for reading the audio resource data. + // 2a. If the returned FD is -1, or equals the audio map, or + // equals the audio bundle, try to get the offset of the + // data from the audio map, using the Audio36 name. + // + // If the returned offset is -1, this is not a valid resource; + // return 0. Otherwise, set the read offset for the FD to the + // returned offset. + // 2b. Otherwise, use the FD as-is (it is a patch file), with zero + // offset, and record it separately so it can be closed later. + // + // For plain audio: + // + // 1. First, request an Audio resource from the resource cache. If + // one does not exist, make the same request for a Wave resource. + // 2a. If an audio resource was discovered, record its memory ID + // and clear the streaming FD + // 2b. Otherwise, request an Audio FD. If one does not exist, make + // the same request for a Wave FD. If neither exist, this is not + // a valid resource; return 0. Otherwise, use the returned FD as + // the streaming ID and set the memory ID to null. + // + // Once these steps are complete, the audio engine either has a file + // descriptor + offset that it can use to read streamed audio, or it + // has a memory ID that it can use to read cached audio. + // + // Here in ScummVM we just ask the resource manager to give us the + // resource and we get a seekable stream. + + // TODO: This should be fixed to use streaming, which means + // fixing the resource manager to allow streaming, which means + // probably rewriting a bunch of the resource manager. + Resource *resource = _resMan->findResource(resourceId, true); + if (resource == nullptr) { + return 0; + } + + channelIndex = _numActiveChannels++; + + AudioChannel &channel = getChannel(channelIndex); + channel.id = resourceId; + channel.resource = resource; + channel.loop = loop; + channel.robot = false; + channel.vmd = false; + channel.lastFadeTick = 0; + channel.fadeStepsRemaining = 0; + channel.soundNode = soundNode; + channel.volume = volume < 0 || volume > kMaxVolume ? (int)kMaxVolume : volume; + // TODO: SCI3 introduces stereo audio + channel.pan = -1; + + if (monitor) { + _monitoredChannelIndex = channelIndex; + } + + Common::MemoryReadStream headerStream(resource->_header, resource->_headerSize, DisposeAfterUse::NO); + Common::SeekableReadStream *dataStream = channel.resourceStream = resource->makeStream(); + + if (detectSolAudio(headerStream)) { + channel.stream = makeSOLStream(&headerStream, dataStream, DisposeAfterUse::NO); + } else if (detectWaveAudio(*dataStream)) { + channel.stream = Audio::makeWAVStream(dataStream, DisposeAfterUse::NO); + } else { + byte flags = Audio::FLAG_LITTLE_ENDIAN; + if (_globalBitDepth == 16) { + flags |= Audio::FLAG_16BITS; + } else { + flags |= Audio::FLAG_UNSIGNED; + } + + if (_globalNumOutputChannels == 2) { + flags |= Audio::FLAG_STEREO; + } + + channel.stream = Audio::makeRawStream(dataStream, _globalSampleRate, flags, DisposeAfterUse::NO); + } + + channel.converter = Audio::makeRateConverter(channel.stream->getRate(), getRate(), channel.stream->isStereo(), false); + + // NOTE: SCI engine sets up a decompression buffer here for the audio + // stream, plus writes information about the sample to the channel to + // convert to the correct hardware output format, and allocates the + // monitoring buffer to match the bitrate/samplerate/channels of the + // original stream. We do not need to do any of these things since we + // use audio streams, and allocate and fill the monitoring buffer + // when reading audio data from the stream. + + channel.duration = /* round up */ 1 + (channel.stream->getLength().msecs() * 60 / 1000); + + const uint32 now = g_sci->getTickCount(); + if (!autoPlay) { + channel.pausedAtTick = now; + } + channel.startedAtTick = now; + + if (_numActiveChannels == 1) { + _startedAtTick = now; + _mixer->pauseHandle(_handle, false); + } + + return channel.duration; +} + +bool Audio32::resume(const int16 channelIndex) { + if (channelIndex == kNoExistingChannel) { + return false; + } + + Common::StackLock lock(_mutex); + const uint32 now = g_sci->getTickCount(); + + if (channelIndex == kAllChannels) { + // Global pause in SSCI is an extra layer over + // individual channel pauses, so only unpause channels + // if there was not a global pause in place + if (_pausedAtTick == 0) { + return false; + } + + for (int i = 0; i < _numActiveChannels; ++i) { + AudioChannel &channel = getChannel(i); + if (!channel.pausedAtTick) { + channel.startedAtTick += now - _pausedAtTick; + } + } + + _startedAtTick += now - _pausedAtTick; + _pausedAtTick = 0; + _mixer->pauseHandle(_handle, false); + return true; + } else if (channelIndex == kRobotChannel) { + for (int i = 0; i < _numActiveChannels; ++i) { + AudioChannel &channel = getChannel(i); + if (channel.robot) { + channel.startedAtTick += now - channel.pausedAtTick; + channel.pausedAtTick = 0; + // TODO: Robot + // StartRobot(); + return true; + } + } + } else { + AudioChannel &channel = getChannel(channelIndex); + if (channel.pausedAtTick) { + channel.startedAtTick += now - channel.pausedAtTick; + channel.pausedAtTick = 0; + return true; + } + } + + return false; +} + +bool Audio32::pause(const int16 channelIndex) { + if (channelIndex == kNoExistingChannel) { + return false; + } + + Common::StackLock lock(_mutex); + const uint32 now = g_sci->getTickCount(); + bool didPause = false; + + if (channelIndex == kAllChannels) { + if (_pausedAtTick == 0) { + _pausedAtTick = now; + _mixer->pauseHandle(_handle, true); + didPause = true; + } + } else if (channelIndex == kRobotChannel) { + _robotAudioPaused = true; + for (int16 i = 0; i < _numActiveChannels; ++i) { + AudioChannel &channel = getChannel(i); + if (channel.robot) { + channel.pausedAtTick = now; + } + } + + // NOTE: The actual engine returns false here regardless of whether + // or not channels were paused + } else { + AudioChannel &channel = getChannel(channelIndex); + + if (channel.pausedAtTick == 0) { + channel.pausedAtTick = now; + didPause = true; + } + } + + return didPause; +} + +int16 Audio32::stop(const int16 channelIndex) { + Common::StackLock lock(_mutex); + const int16 oldNumChannels = _numActiveChannels; + + if (channelIndex == kNoExistingChannel || oldNumChannels == 0) { + return 0; + } + + if (channelIndex == kAllChannels) { + for (int i = 0; i < oldNumChannels; ++i) { + freeChannel(i); + } + _numActiveChannels = 0; + } else { + freeChannel(channelIndex); + --_numActiveChannels; + for (int i = channelIndex; i < oldNumChannels - 1; ++i) { + _channels[i] = _channels[i + 1]; + if (i + 1 == _monitoredChannelIndex) { + _monitoredChannelIndex = i; + } + } + } + + // NOTE: SSCI stops the DSP interrupt and frees the + // global decompression buffer here if there are no + // more active channels + if (_numActiveChannels == 0) { + _mixer->pauseHandle(_handle, true); + } + + return oldNumChannels; +} + +int16 Audio32::getPosition(const int16 channelIndex) const { + Common::StackLock lock(_mutex); + if (channelIndex == kNoExistingChannel || _numActiveChannels == 0) { + return -1; + } + + // NOTE: SSCI treats this as an unsigned short except for + // when the value is 65535, then it treats it as signed + int position = -1; + const uint32 now = g_sci->getTickCount(); + + // NOTE: The original engine also queried the audio driver to see whether + // it thought that there was audio playback occurring via driver opcode 9 + if (channelIndex == kAllChannels) { + if (_pausedAtTick) { + position = _pausedAtTick - _startedAtTick; + } else { + position = now - _startedAtTick; + } + } else { + const AudioChannel &channel = getChannel(channelIndex); + + if (channel.pausedAtTick) { + position = channel.pausedAtTick - channel.startedAtTick; + } else if (_pausedAtTick) { + position = _pausedAtTick - channel.startedAtTick; + } else { + position = now - channel.startedAtTick; + } + } + + return MIN(position, 65534); +} + +void Audio32::setLoop(const int16 channelIndex, const bool loop) { + Common::StackLock lock(_mutex); + + if (channelIndex < 0 || channelIndex >= _numActiveChannels) { + return; + } + + AudioChannel &channel = getChannel(channelIndex); + channel.loop = loop; +} + +reg_t Audio32::kernelPlay(const bool autoPlay, const int argc, const reg_t *const argv) { + if (argc == 0) { + return make_reg(0, _numActiveChannels); + } + + const int16 channelIndex = findChannelByArgs(argc, argv, 0, NULL_REG); + ResourceId resourceId; + bool loop; + int16 volume; + bool monitor = false; + reg_t soundNode; + + if (argc >= 5) { + resourceId = ResourceId(kResourceTypeAudio36, argv[0].toUint16(), argv[1].toUint16(), argv[2].toUint16(), argv[3].toUint16(), argv[4].toUint16()); + + if (argc < 6 || argv[5].toSint16() == 1) { + loop = false; + } else { + // NOTE: Uses -1 for infinite loop. Presumably the + // engine was supposed to allow counter loops at one + // point, but ended up only using loop as a boolean. + loop = (bool)argv[5].toSint16(); + } + + if (argc < 7 || argv[6].toSint16() < 0 || argv[6].toSint16() > Audio32::kMaxVolume) { + volume = Audio32::kMaxVolume; + + if (argc >= 7) { + monitor = true; + } + } else { + volume = argv[6].toSint16(); + } + } else { + resourceId = ResourceId(kResourceTypeAudio, argv[0].toUint16()); + + if (argc < 2 || argv[1].toSint16() == 1) { + loop = false; + } else { + loop = (bool)argv[1].toSint16(); + } + + // TODO: SCI3 uses the 0x80 bit as a flag to + // indicate "priority channel", but the volume is clamped + // in this call to 0x7F so that flag never makes it into + // the audio subsystem + if (argc < 3 || argv[2].toSint16() < 0 || argv[2].toSint16() > Audio32::kMaxVolume) { + volume = Audio32::kMaxVolume; + + if (argc >= 3) { + monitor = true; + } + } else { + volume = argv[2].toSint16(); + } + + soundNode = argc == 4 ? argv[3] : NULL_REG; + } + + return make_reg(0, play(channelIndex, resourceId, autoPlay, loop, volume, soundNode, monitor)); +} + +#pragma mark - +#pragma mark Effects + +int16 Audio32::getVolume(const int16 channelIndex) const { + Common::StackLock lock(_mutex); + if (channelIndex < 0 || channelIndex >= _numActiveChannels) { + return _mixer->getChannelVolume(_handle) * kMaxVolume / Audio::Mixer::kMaxChannelVolume; + } + + return getChannel(channelIndex).volume; +} + +void Audio32::setVolume(const int16 channelIndex, int16 volume) { + Common::StackLock lock(_mutex); + + volume = MIN((int16)kMaxVolume, volume); + if (channelIndex == kAllChannels) { + ConfMan.setInt("sfx_volume", volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume); + ConfMan.setInt("speech_volume", volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume); + _mixer->setChannelVolume(_handle, volume * Audio::Mixer::kMaxChannelVolume / kMaxVolume); + g_engine->syncSoundSettings(); + } else if (channelIndex != kNoExistingChannel) { + getChannel(channelIndex).volume = volume; + } +} + +bool Audio32::fadeChannel(const int16 channelIndex, const int16 targetVolume, const int16 speed, const int16 steps, const bool stopAfterFade) { + Common::StackLock lock(_mutex); + + if (channelIndex < 0 || channelIndex >= _numActiveChannels) { + return false; + } + + AudioChannel &channel = getChannel(channelIndex); + + if (channel.id.getType() != kResourceTypeAudio || channel.volume == targetVolume) { + return false; + } + + if (steps) { + channel.fadeVolume = targetVolume; + channel.fadeSpeed = speed; + channel.fadeStepsRemaining = steps; + channel.stopChannelOnFade = stopAfterFade; + channel.lastFadeTick = g_sci->getTickCount(); + } else { + setVolume(channelIndex, targetVolume); + } + + return true; +} + +bool Audio32::processFade(const int16 channelIndex) { + Common::StackLock lock(_mutex); + AudioChannel &channel = getChannel(channelIndex); + + uint32 now = g_sci->getTickCount(); + + if (channel.lastFadeTick + channel.fadeSpeed <= now) { + --channel.fadeStepsRemaining; + + if (!channel.fadeStepsRemaining) { + if (channel.stopChannelOnFade) { + stop(channelIndex); + return true; + } else { + setVolume(channelIndex, channel.fadeVolume); + } + } else { + int volume = channel.volume - (channel.volume - channel.fadeVolume) / (channel.fadeStepsRemaining + 1); + + if (volume == channel.fadeVolume) { + channel.fadeStepsRemaining = 1; + } + + setVolume(channelIndex, volume); + channel.lastFadeTick = now; + } + } + + return false; +} + +#pragma mark - +#pragma mark Signal monitoring + +bool Audio32::hasSignal() const { + Common::StackLock lock(_mutex); + + if (_monitoredChannelIndex == -1) { + return false; + } + + const Audio::st_sample_t *buffer = _monitoredBuffer; + const Audio::st_sample_t *const end = _monitoredBuffer + _numMonitoredSamples; + + while (buffer != end) { + const Audio::st_sample_t sample = *buffer++; + if (sample > 1280 || sample < -1280) { + return true; + } + } + + return false; +} + +} // End of namespace Sci diff --git a/engines/sci/sound/audio32.h b/engines/sci/sound/audio32.h new file mode 100644 index 00000000000..59aba66e2c9 --- /dev/null +++ b/engines/sci/sound/audio32.h @@ -0,0 +1,566 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SCI_AUDIO32_H +#define SCI_AUDIO32_H +#include "audio/audiostream.h" // for AudioStream, SeekableAudioStream (... +#include "audio/mixer.h" // for Mixer, SoundHandle +#include "audio/rate.h" // for Audio::st_volume_t, RateConverter +#include "common/array.h" // for Array +#include "common/mutex.h" // for StackLock, Mutex +#include "common/scummsys.h" // for int16, uint8, uint32, uint16 +#include "engines/sci/resource.h" // for ResourceId +#include "sci/engine/vm_types.h" // for reg_t, NULL_REG + +namespace Sci { + +/** + * An audio channel used by the software SCI mixer. + */ +struct AudioChannel { + /** + * The ID of the resource loaded into this channel. + */ + ResourceId id; + + /** + * The resource loaded into this channel. + */ + Resource *resource; + + /** + * Data stream containing the raw audio for the channel. + */ + Common::SeekableReadStream *resourceStream; + + /** + * The audio stream loaded into this channel. + * `SeekableAudioStream` is used here instead of + * `RewindableAudioStream` because + * `RewindableAudioStream` does not include the + * `getLength` function, which is needed to tell the + * game engine the duration of audio streams. + */ + Audio::SeekableAudioStream *stream; + + /** + * The converter used to transform and merge the input + * stream into the mixer's output buffer. + */ + Audio::RateConverter *converter; + + /** + * Duration of the channel, in ticks. + */ + uint32 duration; + + /** + * The tick when the channel was started. + */ + uint32 startedAtTick; + + /** + * The tick when the channel was paused. + */ + uint32 pausedAtTick; + + /** + * Whether or not the audio in this channel should loop + * infinitely. + */ + bool loop; + + /** + * The time the last fade iteration occurred. + */ + uint32 lastFadeTick; + + /** + * The target volume of the fade. + */ + int fadeVolume; + + /** + * The number of ticks that should elapse between + * each change of volume. + */ + int fadeSpeed; + + /** + * The number of iterations the fade should take to + * complete. If this value is 0, it indicates that the + * channel is not fading. + */ + int fadeStepsRemaining; + + /** + * Whether or not the channel should be stopped and + * freed when the fade is complete. + */ + bool stopChannelOnFade; + + /** + * Whether or not this channel contains a Robot + * audio block. + */ + bool robot; + + /** + * Whether or not this channel contains a VMD audio + * track. + */ + bool vmd; + + /** + * For digital sound effects, the related VM + * Sound::nodePtr object for the sound. + */ + reg_t soundNode; + + /** + * The playback volume, from 1 to 127 inclusive. + */ + int volume; + + /** + * The amount to pan to the right, from 0 to 100. + * 50 is centered, -1 is not panned. + */ + int pan; +}; + +/** + * Special audio channel indexes used to select a channel + * for digital audio playback. + */ +enum AudioChannelIndex { + kRobotChannel = -3, + kNoExistingChannel = -2, + kAllChannels = -1 +}; + +/** + * Audio32 acts as a permanent audio stream into the system + * mixer and provides digital audio services for the SCI32 + * engine, since the system mixer does not support all the + * features of SCI. + */ +class Audio32 : public Audio::AudioStream { +public: + Audio32(ResourceManager *resMan); + ~Audio32(); + +private: + ResourceManager *_resMan; + Audio::Mixer *_mixer; + Audio::SoundHandle _handle; + Common::Mutex _mutex; + + enum { + /** + * The maximum channel volume. + */ + kMaxVolume = 127 + }; + +#pragma mark - +#pragma mark AudioStream implementation +public: + int readBuffer(Audio::st_sample_t *const buffer, const int numSamples); + bool isStereo() const { return true; } + int getRate() const { return _mixer->getOutputRate(); } + bool endOfData() const { return _numActiveChannels == 0; } + bool endOfStream() const { return false; } + +private: + /** + * Mixes audio from the given source stream into the + * target buffer using the given rate converter. + */ + int writeAudioInternal(Audio::RewindableAudioStream *const sourceStream, Audio::RateConverter *const converter, Audio::st_sample_t *targetBuffer, const int numSamples, const Audio::st_volume_t leftVolume, const Audio::st_volume_t rightVolume, const bool loop); + +#pragma mark - +#pragma mark Channel management +public: + /** + * Gets the number of currently active channels. + */ + inline uint8 getNumActiveChannels() const { + Common::StackLock lock(_mutex); + return _numActiveChannels; + } + + /** + * Finds a channel that is already configured for the + * given audio sample. + * + * @param startIndex The location of the audio resource + * information in the arguments list. + */ + int16 findChannelByArgs(int argc, const reg_t *argv, const int startIndex, const reg_t soundNode) const; + + /** + * Finds a channel that is already configured for the + * given audio sample. + */ + int16 findChannelById(const ResourceId resourceId, const reg_t soundNode = NULL_REG) const; + +private: + /** + * The audio channels. + */ + Common::Array _channels; + + /** + * The number of active audio channels in the mixer. + * Being active is not the same as playing; active + * channels may be paused. + */ + uint8 _numActiveChannels; + + /** + * Gets the audio channel at the given index. + */ + inline AudioChannel &getChannel(const int16 channelIndex) { + Common::StackLock lock(_mutex); + assert(channelIndex >= 0 && channelIndex < _numActiveChannels); + return _channels[channelIndex]; + } + + /** + * Gets the audio channel at the given index. + */ + inline const AudioChannel &getChannel(const int16 channelIndex) const { + Common::StackLock lock(_mutex); + assert(channelIndex >= 0 && channelIndex < _numActiveChannels); + return _channels[channelIndex]; + } + + /** + * Frees all non-looping channels that have reached the + * end of their stream. + */ + void freeUnusedChannels(); + + /** + * Frees resources allocated to the given channel. + */ + void freeChannel(const int16 channelIndex); + +#pragma mark - +#pragma mark Script compatibility +public: + /** + * Gets the (fake) sample rate of the hardware DAC. + * For script compatibility only. + */ + inline uint16 getSampleRate() const { + return _globalSampleRate; + } + + /** + * Sets the (fake) sample rate of the hardware DAC. + * For script compatibility only. + */ + void setSampleRate(uint16 rate); + + /** + * Gets the (fake) bit depth of the hardware DAC. + * For script compatibility only. + */ + inline uint8 getBitDepth() const { + return _globalBitDepth; + } + + /** + * Sets the (fake) sample rate of the hardware DAC. + * For script compatibility only. + */ + void setBitDepth(uint8 depth); + + /** + * Gets the (fake) number of output (speaker) channels + * of the hardware DAC. For script compatibility only. + */ + inline uint8 getNumOutputChannels() const { + return _globalNumOutputChannels; + } + + /** + * Sets the (fake) number of output (speaker) channels + * of the hardware DAC. For script compatibility only. + */ + void setNumOutputChannels(int16 numChannels); + + /** + * Gets the (fake) number of preloaded channels. + * For script compatibility only. + */ + inline uint8 getPreload() const { + return _preload; + } + + /** + * Sets the (fake) number of preloaded channels. + * For script compatibility only. + */ + inline void setPreload(uint8 preload) { + _preload = preload; + } + +private: + /** + * The hardware DAC sample rate. Stored only for script + * compatibility. + */ + uint16 _globalSampleRate; + + /** + * The maximum allowed sample rate of the system mixer. + * Stored only for script compatibility. + */ + uint16 _maxAllowedSampleRate; + + /** + * The hardware DAC bit depth. Stored only for script + * compatibility. + */ + uint8 _globalBitDepth; + + /** + * The maximum allowed bit depth of the system mixer. + * Stored only for script compatibility. + */ + uint8 _maxAllowedBitDepth; + + /** + * The hardware DAC output (speaker) channel + * configuration. Stored only for script compatibility. + */ + uint8 _globalNumOutputChannels; + + /** + * The maximum allowed number of output (speaker) + * channels of the system mixer. Stored only for script + * compatibility. + */ + uint8 _maxAllowedOutputChannels; + + /** + * The number of audio channels that should have their + * data preloaded into memory instead of streaming from + * disk. + * 1 = all channels, 2 = 2nd active channel and above, + * etc. + * Stored only for script compatibility. + */ + uint8 _preload; + +#pragma mark - +#pragma mark Robot +public: + +private: + /** + * When true, channels marked as robot audio will not be + * played. + */ + bool _robotAudioPaused; + +#pragma mark - +#pragma mark Playback +public: + /** + * Starts or resumes playback of an audio channel. + */ + uint16 play(int16 channelIndex, const ResourceId resourceId, const bool autoPlay, const bool loop, const int16 volume, const reg_t soundNode, const bool monitor); + + /** + * Resumes playback of a paused audio channel, or of + * the entire audio player. + */ + bool resume(const int16 channelIndex); + bool resume(const ResourceId resourceId, const reg_t soundNode = NULL_REG) { + Common::StackLock lock(_mutex); + return resume(findChannelById(resourceId, soundNode)); + } + + /** + * Pauses an audio channel, or the entire audio player. + */ + bool pause(const int16 channelIndex); + bool pause(const ResourceId resourceId, const reg_t soundNode = NULL_REG) { + Common::StackLock lock(_mutex); + return pause(findChannelById(resourceId, soundNode)); + } + + /** + * Stops and unloads an audio channel, or the entire + * audio player. + */ + int16 stop(const int16 channelIndex); + int16 stop(const ResourceId resourceId, const reg_t soundNode = NULL_REG) { + Common::StackLock lock(_mutex); + return stop(findChannelById(resourceId, soundNode)); + } + + /** + * Returns the playback position for the given channel + * number, in ticks. + */ + int16 getPosition(const int16 channelIndex) const; + int16 getPosition(const ResourceId resourceId, const reg_t soundNode = NULL_REG) { + Common::StackLock lock(_mutex); + return getPosition(findChannelById(resourceId, soundNode)); + } + + /** + * Sets whether or not the given channel should loop. + */ + void setLoop(const int16 channelIndex, const bool loop); + void setLoop(const ResourceId resourceId, const reg_t soundNode, const bool loop) { + Common::StackLock lock(_mutex); + setLoop(findChannelById(resourceId, soundNode), loop); + } + + reg_t kernelPlay(const bool autoPlay, const int argc, const reg_t *const argv); + +private: + /** + * The tick when audio was globally paused. + */ + uint32 _pausedAtTick; + + /** + * The tick when audio was globally started. + */ + uint32 _startedAtTick; + +#pragma mark - +#pragma mark Effects +public: + /** + * Gets the volume for a given channel. Passing + * `kAllChannels` will get the global volume. + */ + int16 getVolume(const int16 channelIndex) const; + int16 getVolume(const ResourceId resourceId, const reg_t soundNode) const { + Common::StackLock lock(_mutex); + return getVolume(findChannelById(resourceId, soundNode)); + } + + /** + * Sets the volume of an audio channel. Passing + * `kAllChannels` will set the global volume. + */ + void setVolume(const int16 channelIndex, int16 volume); + void setVolume(const ResourceId resourceId, const reg_t soundNode, const int16 volume) { + Common::StackLock lock(_mutex); + setVolume(findChannelById(resourceId, soundNode), volume); + } + + /** + * Initiate an immediate fade of the given channel. + */ + bool fadeChannel(const int16 channelIndex, const int16 targetVolume, const int16 speed, const int16 steps, const bool stopAfterFade); + bool fadeChannel(const ResourceId resourceId, const reg_t soundNode, const int16 targetVolume, const int16 speed, const int16 steps, const bool stopAfterFade) { + Common::StackLock lock(_mutex); + return fadeChannel(findChannelById(resourceId, soundNode), targetVolume, speed, steps, stopAfterFade); + } + + /** + * Gets whether attenuated mixing mode is active. + */ + inline bool getAttenuatedMixing() const { + return _attenuatedMixing; + } + + /** + * Sets the attenuated mixing mode. + */ + void setAttenuatedMixing(bool attenuated) { + Common::StackLock lock(_mutex); + _attenuatedMixing = attenuated; + } + +private: + /** + * If true, audio will be mixed by reducing the target + * buffer by half every time a new channel is mixed in. + * The final channel is not attenuated. + */ + bool _attenuatedMixing; + + /** + * When true, a modified attenuation algorithm is used + * (`A/4 + B`) instead of standard linear attenuation + * (`A/2 + B/2`). + */ + bool _useModifiedAttenuation; + + /** + * Processes an audio fade for the given channel. + * + * @returns true if the fade was completed and the + * channel was stopped. + */ + bool processFade(const int16 channelIndex); + +#pragma mark - +#pragma mark Signal monitoring +public: + /** + * Returns whether the currently monitored audio channel + * contains any signal within the next audio frame. + */ + bool hasSignal() const; + +private: + /** + * The index of the channel being monitored for signal, + * or -1 if no channel is monitored. When a channel is + * monitored, it also causes the engine to play only the + * monitored channel. + */ + int16 _monitoredChannelIndex; + + /** + * The data buffer holding decompressed audio data for + * the channel that will be monitored for an audio + * signal. + */ + Audio::st_sample_t *_monitoredBuffer; + + /** + * The size of the buffer, in bytes. + */ + size_t _monitoredBufferSize; + + /** + * The number of valid audio samples in the signal + * monitoring buffer. + */ + int _numMonitoredSamples; +}; + +} // End of namespace Sci +#endif diff --git a/engines/sci/sound/decoders/sol.cpp b/engines/sci/sound/decoders/sol.cpp new file mode 100644 index 00000000000..280b24fd3a1 --- /dev/null +++ b/engines/sci/sound/decoders/sol.cpp @@ -0,0 +1,272 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" +#include "common/substream.h" +#include "common/util.h" +#include "engines/sci/sci.h" +#include "engines/sci/sound/decoders/sol.h" + +namespace Sci { + +// Note that the 16-bit version is also used in coktelvideo.cpp +static const uint16 tableDPCM16[128] = { + 0x0000, 0x0008, 0x0010, 0x0020, 0x0030, 0x0040, 0x0050, 0x0060, 0x0070, 0x0080, + 0x0090, 0x00A0, 0x00B0, 0x00C0, 0x00D0, 0x00E0, 0x00F0, 0x0100, 0x0110, 0x0120, + 0x0130, 0x0140, 0x0150, 0x0160, 0x0170, 0x0180, 0x0190, 0x01A0, 0x01B0, 0x01C0, + 0x01D0, 0x01E0, 0x01F0, 0x0200, 0x0208, 0x0210, 0x0218, 0x0220, 0x0228, 0x0230, + 0x0238, 0x0240, 0x0248, 0x0250, 0x0258, 0x0260, 0x0268, 0x0270, 0x0278, 0x0280, + 0x0288, 0x0290, 0x0298, 0x02A0, 0x02A8, 0x02B0, 0x02B8, 0x02C0, 0x02C8, 0x02D0, + 0x02D8, 0x02E0, 0x02E8, 0x02F0, 0x02F8, 0x0300, 0x0308, 0x0310, 0x0318, 0x0320, + 0x0328, 0x0330, 0x0338, 0x0340, 0x0348, 0x0350, 0x0358, 0x0360, 0x0368, 0x0370, + 0x0378, 0x0380, 0x0388, 0x0390, 0x0398, 0x03A0, 0x03A8, 0x03B0, 0x03B8, 0x03C0, + 0x03C8, 0x03D0, 0x03D8, 0x03E0, 0x03E8, 0x03F0, 0x03F8, 0x0400, 0x0440, 0x0480, + 0x04C0, 0x0500, 0x0540, 0x0580, 0x05C0, 0x0600, 0x0640, 0x0680, 0x06C0, 0x0700, + 0x0740, 0x0780, 0x07C0, 0x0800, 0x0900, 0x0A00, 0x0B00, 0x0C00, 0x0D00, 0x0E00, + 0x0F00, 0x1000, 0x1400, 0x1800, 0x1C00, 0x2000, 0x3000, 0x4000 +}; + +static const byte tableDPCM8[8] = { 0, 1, 2, 3, 6, 10, 15, 21 }; + +/** + * Decompresses 16-bit DPCM compressed audio. Each byte read + * outputs one sample into the decompression buffer. + */ +static void deDPCM16(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, int16 &sample) { + for (uint32 i = 0; i < numBytes; ++i) { + const uint8 delta = audioStream.readByte(); + if (delta & 0x80) { + sample -= tableDPCM16[delta & 0x7f]; + } else { + sample += tableDPCM16[delta]; + } + sample = CLIP(sample, -32768, 32767); + *out++ = TO_LE_16(sample); + } +} + +/** + * Decompresses one half of an 8-bit DPCM compressed audio + * byte. + */ +static void deDPCM8Nibble(int16 *out, uint8 &sample, uint8 delta) { + if (delta & 8) { + sample -= tableDPCM8[delta & 7]; + } else { + sample += tableDPCM8[delta & 7]; + } + sample = CLIP(sample, 0, 255); + *out = (sample << 8) ^ 0x8000; +} + +/** + * Decompresses 8-bit DPCM compressed audio. Each byte read + * outputs two samples into the decompression buffer. + */ +static void deDPCM8(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, uint8 &sample) { + for (uint32 i = 0; i < numBytes; ++i) { + const uint8 delta = audioStream.readByte(); + deDPCM8Nibble(out++, sample, delta >> 4); + deDPCM8Nibble(out++, sample, delta & 0xf); + } +} + +# pragma mark - + +template +SOLStream::SOLStream(Common::SeekableReadStream *stream, const DisposeAfterUse::Flag disposeAfterUse, const int32 dataOffset, const uint16 sampleRate, const int32 rawDataSize) : + _stream(stream, disposeAfterUse), + _dataOffset(dataOffset), + _sampleRate(sampleRate), + // SSCI aligns the size of SOL data to 32 bits + _rawDataSize(rawDataSize & ~3) { + // TODO: This is not valid for stereo SOL files, which + // have interleaved L/R compression so need to store the + // carried values for each channel separately. See + // 60900.aud from Lighthouse for an example stereo file + if (S16BIT) { + _dpcmCarry16 = 0; + } else { + _dpcmCarry8 = 0x80; + } + + const uint8 compressionRatio = 2; + const uint8 numChannels = STEREO ? 2 : 1; + const uint8 bytesPerSample = S16BIT ? 2 : 1; + _length = Audio::Timestamp((_rawDataSize * compressionRatio * 1000) / (_sampleRate * numChannels * bytesPerSample), 60); + } + +template +bool SOLStream::seek(const Audio::Timestamp &where) override { + if (where != 0) { + // In order to seek in compressed SOL files, all + // previous bytes must be known since it uses + // differential compression. Therefore, only seeking + // to the beginning is supported now (SSCI does not + // offer seeking anyway) + return false; + } + + if (S16BIT) { + _dpcmCarry16 = 0; + } else { + _dpcmCarry8 = 0x80; + } + + return _stream->seek(_dataOffset, SEEK_SET); +} + +template +Audio::Timestamp SOLStream::getLength() const override { + return _length; +} + +template +int SOLStream::readBuffer(int16 *buffer, const int numSamples) override { + // Reading an odd number of 8-bit samples will result in a loss of samples + // since one byte represents two samples and we do not store the second + // nibble in this case; it should never happen in reality + assert(S16BIT || (numSamples % 2) == 0); + + const int samplesPerByte = S16BIT ? 1 : 2; + + int32 bytesToRead = numSamples / samplesPerByte; + if (_stream->pos() + bytesToRead > _rawDataSize) { + bytesToRead = _rawDataSize - _stream->pos(); + } + + if (S16BIT) { + deDPCM16(buffer, *_stream, bytesToRead, _dpcmCarry16); + } else { + deDPCM8(buffer, *_stream, bytesToRead, _dpcmCarry8); + } + + const int samplesRead = bytesToRead * samplesPerByte; + return samplesRead; +} + +template +bool SOLStream::isStereo() const override { + return STEREO; +} + +template +int SOLStream::getRate() const override { + return _sampleRate; +} + +template +bool SOLStream::endOfData() const override { + return _stream->eos() || _stream->pos() >= _dataOffset + _rawDataSize; +} + +template +bool SOLStream::rewind() override { + return seek(0); +} + +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) { + + // TODO: Might not be necessary? Makes seeking work, but + // not sure if audio is ever actually seeked in SSCI. + const int32 initialPosition = stream->pos(); + + byte header[6]; + if (stream->read(header, sizeof(header)) != sizeof(header)) { + return nullptr; + } + + if (header[0] != 0x8d || READ_BE_UINT32(header + 2) != MKTAG('S', 'O', 'L', 0)) { + return nullptr; + } + + const uint8 headerSize = header[1]; + const uint16 sampleRate = stream->readUint16LE(); + const byte flags = stream->readByte(); + const uint32 dataSize = stream->readUint32LE(); + + if (flags & kCompressed) { + if (flags & kStereo && flags & k16Bit) { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else if (flags & kStereo) { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else if (flags & k16Bit) { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else { + return new SOLStream(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } + } + + byte rawFlags = Audio::FLAG_LITTLE_ENDIAN; + if (flags & k16Bit) { + rawFlags |= Audio::FLAG_16BITS; + } else { + rawFlags |= Audio::FLAG_UNSIGNED; + } + + if (flags & kStereo) { + rawFlags |= Audio::FLAG_STEREO; + } + + return Audio::makeRawStream(new Common::SeekableSubReadStream(stream, initialPosition + headerSize, initialPosition + headerSize + dataSize, disposeAfterUse), sampleRate, rawFlags, disposeAfterUse); +} + +// TODO: This needs to be removed when resource manager is fixed +// to not split audio into two parts +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *headerStream, Common::SeekableReadStream *dataStream, DisposeAfterUse::Flag disposeAfterUse) { + + if (headerStream->readUint32BE() != MKTAG('S', 'O', 'L', 0)) { + return nullptr; + } + + const uint16 sampleRate = headerStream->readUint16LE(); + const byte flags = headerStream->readByte(); + const int32 dataSize = headerStream->readSint32LE(); + + if (flags & kCompressed) { + if (flags & kStereo && flags & k16Bit) { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else if (flags & kStereo) { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else if (flags & k16Bit) { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else { + return new SOLStream(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } + } + + byte rawFlags = Audio::FLAG_LITTLE_ENDIAN; + if (flags & k16Bit) { + rawFlags |= Audio::FLAG_16BITS; + } else { + rawFlags |= Audio::FLAG_UNSIGNED; + } + + if (flags & kStereo) { + rawFlags |= Audio::FLAG_STEREO; + } + + return Audio::makeRawStream(dataStream, sampleRate, rawFlags, disposeAfterUse); +} + +} diff --git a/engines/sci/sound/decoders/sol.h b/engines/sci/sound/decoders/sol.h new file mode 100644 index 00000000000..1046d0b213c --- /dev/null +++ b/engines/sci/sound/decoders/sol.h @@ -0,0 +1,89 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef SCI_SOUND_DECODERS_SOL_H +#define SCI_SOUND_DECODERS_SOL_H +#include "audio/audiostream.h" +#include "common/stream.h" + +namespace Sci { + +enum SOLFlags { + kCompressed = 1, + k16Bit = 4, + kStereo = 16 +}; + +template +class SOLStream : public Audio::SeekableAudioStream { +private: + /** + * Read stream containing possibly-compressed SOL audio. + */ + Common::DisposablePtr _stream; + + /** + * Start offset of the audio data in the read stream. + */ + int32 _dataOffset; + + /** + * Sample rate of audio data. + */ + uint16 _sampleRate; + + /** + * The raw (possibly-compressed) size of audio data in + * the stream. + */ + int32 _rawDataSize; + + /** + * The last sample from the previous DPCM decode. + */ + union { + int16 _dpcmCarry16; + uint8 _dpcmCarry8; + }; + + /** + * The calculated length of the stream. + */ + Audio::Timestamp _length; + + virtual bool seek(const Audio::Timestamp &where) override; + virtual Audio::Timestamp getLength() const override; + virtual int readBuffer(int16 *buffer, const int numSamples) override; + virtual bool isStereo() const override; + virtual int getRate() const override; + virtual bool endOfData() const override; + virtual bool rewind() override; + +public: + SOLStream(Common::SeekableReadStream *stream, const DisposeAfterUse::Flag disposeAfterUse, const int32 dataOffset, const uint16 sampleRate, const int32 rawDataSize); +}; + +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse); + +Audio::SeekableAudioStream *makeSOLStream(Common::SeekableReadStream *headerStream, Common::SeekableReadStream *dataStream, DisposeAfterUse::Flag disposeAfterUse); +} +#endif diff --git a/engines/sci/sound/music.cpp b/engines/sci/sound/music.cpp index 5a11ac386a7..3f34ecc2f83 100644 --- a/engines/sci/sound/music.cpp +++ b/engines/sci/sound/music.cpp @@ -212,6 +212,13 @@ void SciMusic::clearPlayList() { void SciMusic::pauseAll(bool pause) { const MusicList::iterator end = _playList.end(); for (MusicList::iterator i = _playList.begin(); i != end; ++i) { +#ifdef ENABLE_SCI32 + // The entire DAC will have been paused by the caller; + // do not pause the individual samples too + if (_soundVersion >= SCI_VERSION_2_1_EARLY && (*i)->isSample) { + continue; + } +#endif soundToggle(*i, pause); } } @@ -472,7 +479,16 @@ void SciMusic::soundPlay(MusicEntry *pSnd) { } } - if (pSnd->pStreamAud) { + if (pSnd->isSample) { +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + g_sci->_audio32->stop(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj); + + g_sci->_audio32->play(kNoExistingChannel, ResourceId(kResourceTypeAudio, pSnd->resourceId), true, pSnd->loop != 0 && pSnd->loop != 1, pSnd->volume, pSnd->soundObj, false); + + return; + } else +#endif if (!_pMixer->isSoundHandleActive(pSnd->hCurrentAud)) { if ((_currentlyPlayingSample) && (_pMixer->isSoundHandleActive(_currentlyPlayingSample->hCurrentAud))) { // Another sample is already playing, we have to stop that one @@ -550,10 +566,18 @@ void SciMusic::soundStop(MusicEntry *pSnd) { pSnd->status = kSoundStopped; if (_soundVersion <= SCI_VERSION_0_LATE) pSnd->isQueued = false; - if (pSnd->pStreamAud) { - if (_currentlyPlayingSample == pSnd) - _currentlyPlayingSample = NULL; - _pMixer->stopHandle(pSnd->hCurrentAud); + if (pSnd->isSample) { +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + g_sci->_audio32->stop(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj); + } else { +#endif + if (_currentlyPlayingSample == pSnd) + _currentlyPlayingSample = NULL; + _pMixer->stopHandle(pSnd->hCurrentAud); +#ifdef ENABLE_SCI32 + } +#endif } if (pSnd->pMidiParser) { @@ -572,9 +596,12 @@ void SciMusic::soundStop(MusicEntry *pSnd) { void SciMusic::soundSetVolume(MusicEntry *pSnd, byte volume) { assert(volume <= MUSIC_VOLUME_MAX); - if (pSnd->pStreamAud) { - // we simply ignore volume changes for samples, because sierra sci also - // doesn't support volume for samples via kDoSound + if (pSnd->isSample) { +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + g_sci->_audio32->setVolume(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj, volume); + } +#endif } else if (pSnd->pMidiParser) { Common::StackLock lock(_mutex); pSnd->pMidiParser->mainThreadBegin(); @@ -614,12 +641,20 @@ void SciMusic::soundKill(MusicEntry *pSnd) { _mutex.unlock(); - if (pSnd->pStreamAud) { - if (_currentlyPlayingSample == pSnd) { - // Forget about this sound, in case it was currently playing - _currentlyPlayingSample = NULL; + if (pSnd->isSample) { +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + g_sci->_audio32->stop(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj); + } else { +#endif + if (_currentlyPlayingSample == pSnd) { + // Forget about this sound, in case it was currently playing + _currentlyPlayingSample = NULL; + } + _pMixer->stopHandle(pSnd->hCurrentAud); +#ifdef ENABLE_SCI32 } - _pMixer->stopHandle(pSnd->hCurrentAud); +#endif delete pSnd->pStreamAud; pSnd->pStreamAud = NULL; delete pSnd->pLoopStream; @@ -685,6 +720,18 @@ void SciMusic::soundResume(MusicEntry *pSnd) { } void SciMusic::soundToggle(MusicEntry *pSnd, bool pause) { +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY && pSnd->isSample) { + if (pause) { + g_sci->_audio32->pause(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj); + } else { + g_sci->_audio32->resume(ResourceId(kResourceTypeAudio, pSnd->resourceId), pSnd->soundObj); + } + + return; + } +#endif + if (pause) soundPause(pSnd); else @@ -813,6 +860,7 @@ MusicEntry::MusicEntry() { pStreamAud = 0; pLoopStream = 0; pMidiParser = 0; + isSample = false; for (int i = 0; i < 16; ++i) { _usedChannels[i] = 0xFF; diff --git a/engines/sci/sound/music.h b/engines/sci/sound/music.h index 047f63b3b7b..3a6de81c495 100644 --- a/engines/sci/sound/music.h +++ b/engines/sci/sound/music.h @@ -31,6 +31,9 @@ #include "sci/sci.h" #include "sci/resource.h" #include "sci/sound/drivers/mididriver.h" +#ifdef ENABLE_SCI32 +#include "sci/sound/audio32.h" +#endif namespace Audio { class LoopingAudioStream; @@ -123,6 +126,7 @@ public: Audio::RewindableAudioStream *pStreamAud; Audio::LoopingAudioStream *pLoopStream; Audio::SoundHandle hCurrentAud; + bool isSample; public: MusicEntry(); diff --git a/engines/sci/sound/soundcmd.cpp b/engines/sci/sound/soundcmd.cpp index efa4735bf40..7d2ab32c758 100644 --- a/engines/sci/sound/soundcmd.cpp +++ b/engines/sci/sound/soundcmd.cpp @@ -23,6 +23,7 @@ #include "common/config-manager.h" #include "audio/audiostream.h" #include "audio/mixer.h" +#include "sci/resource.h" #include "sci/sound/audio.h" #include "sci/sound/music.h" #include "sci/sound/soundcmd.h" @@ -97,12 +98,21 @@ void SoundCommandParser::initSoundResource(MusicEntry *newSound) { // user wants the digital version. if (_useDigitalSFX || !newSound->soundRes) { int sampleLen; - newSound->pStreamAud = _audio->getAudioStream(newSound->resourceId, 65535, &sampleLen); - newSound->soundType = Audio::Mixer::kSFXSoundType; +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + newSound->isSample = g_sci->getResMan()->testResource(ResourceId(kResourceTypeAudio, newSound->resourceId)); + } else { +#endif + newSound->pStreamAud = _audio->getAudioStream(newSound->resourceId, 65535, &sampleLen); + newSound->soundType = Audio::Mixer::kSFXSoundType; + newSound->isSample = newSound->pStreamAud != nullptr; +#ifdef ENABLE_SCI32 + } +#endif } } - if (!newSound->pStreamAud && newSound->soundRes) + if (!newSound->isSample && newSound->soundRes) _music->soundInitSnd(newSound); } @@ -134,7 +144,7 @@ void SoundCommandParser::processInitSound(reg_t obj) { _music->pushBackSlot(newSound); - if (newSound->soundRes || newSound->pStreamAud) { + if (newSound->soundRes || newSound->isSample) { // Notify the engine if (_soundVersion <= SCI_VERSION_0_LATE) writeSelectorValue(_segMan, obj, SELECTOR(state), kSoundInitialized); @@ -314,10 +324,22 @@ reg_t SoundCommandParser::kDoSoundPause(int argc, reg_t *argv, reg_t acc) { } reg_t obj = argv[0]; - uint16 value = argc > 1 ? argv[1].toUint16() : 0; - if (!obj.getSegment()) { // pause the whole playlist - _music->pauseAll(value); - } else { // pause a playlist slot + const bool shouldPause = argc > 1 ? argv[1].toUint16() : false; + if ( + (_soundVersion < SCI_VERSION_2_1_EARLY && !obj.getSegment()) || + (_soundVersion >= SCI_VERSION_2_1_EARLY && obj.isNull()) + ) { + _music->pauseAll(shouldPause); +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + if (shouldPause) { + g_sci->_audio32->pause(kAllChannels); + } else { + g_sci->_audio32->resume(kAllChannels); + } + } +#endif + } else { MusicEntry *musicSlot = _music->getSlot(obj); if (!musicSlot) { // This happens quite frequently @@ -325,7 +347,23 @@ reg_t SoundCommandParser::kDoSoundPause(int argc, reg_t *argv, reg_t acc) { return acc; } - _music->soundToggle(musicSlot, value); +#ifdef ENABLE_SCI32 + // NOTE: The original engine also expected a global + // "kernel call" flag to be true in order to perform + // this action, but the architecture of the ScummVM + // implementation is so different that it doesn't + // matter here + if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) { + const int16 channelIndex = g_sci->_audio32->findChannelById(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj); + + if (shouldPause) { + g_sci->_audio32->pause(channelIndex); + } else { + g_sci->_audio32->resume(channelIndex); + } + } else +#endif + _music->soundToggle(musicSlot, shouldPause); } return acc; } @@ -355,7 +393,11 @@ reg_t SoundCommandParser::kDoSoundMasterVolume(int argc, reg_t *argv, reg_t acc) int vol = CLIP(argv[0].toSint16(), 0, MUSIC_MASTERVOLUME_MAX); vol = vol * Audio::Mixer::kMaxMixerVolume / MUSIC_MASTERVOLUME_MAX; ConfMan.setInt("music_volume", vol); - ConfMan.setInt("sfx_volume", vol); + // In SCI32, digital audio volume is controlled separately by + // kDoAudioVolume + if (_soundVersion < SCI_VERSION_2_1_EARLY) { + ConfMan.setInt("sfx_volume", vol); + } g_engine->syncSoundSettings(); } return acc; @@ -378,6 +420,13 @@ reg_t SoundCommandParser::kDoSoundFade(int argc, reg_t *argv, reg_t acc) { int volume = musicSlot->volume; +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) { + g_sci->_audio32->fadeChannel(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, argv[2].toSint16(), argv[3].toSint16(), argv[4].toSint16(), (bool)argv[5].toSint16()); + return acc; + } +#endif + // If sound is not playing currently, set signal directly if (musicSlot->status != kSoundPlaying) { debugC(kDebugLevelSound, "kDoSound(fade): %04x:%04x fading requested, but sound is currently not playing", PRINT_REG(obj)); @@ -466,7 +515,18 @@ void SoundCommandParser::processUpdateCues(reg_t obj) { return; } - if (musicSlot->pStreamAud) { + if (musicSlot->isSample) { +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + const int position = g_sci->_audio32->getPosition(g_sci->_audio32->findChannelById(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj)); + + if (position == -1) { + processStopSound(musicSlot->soundObj, true); + } + + return; + } +#endif // Update digital sound effect slots uint currentLoopCounter = 0; @@ -669,6 +729,12 @@ reg_t SoundCommandParser::kDoSoundSetVolume(int argc, reg_t *argv, reg_t acc) { value = CLIP(value, 0, MUSIC_VOLUME_MAX); +#ifdef ENABLE_SCI32 + // SSCI unconditionally sets volume if it is digital audio + if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) { + _music->soundSetVolume(musicSlot, value); + } else +#endif if (musicSlot->volume != value) { musicSlot->volume = value; _music->soundSetVolume(musicSlot, value); @@ -727,6 +793,15 @@ reg_t SoundCommandParser::kDoSoundSetLoop(int argc, reg_t *argv, reg_t acc) { } return acc; } + +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY) { + if (value != -1) { + value = 1; + } + } +#endif + if (value == -1) { musicSlot->loop = 0xFFFF; } else { @@ -734,6 +809,13 @@ reg_t SoundCommandParser::kDoSoundSetLoop(int argc, reg_t *argv, reg_t acc) { } writeSelectorValue(_segMan, obj, SELECTOR(loop), musicSlot->loop); + +#ifdef ENABLE_SCI32 + if (_soundVersion >= SCI_VERSION_2_1_EARLY && musicSlot->isSample) { + g_sci->_audio32->setLoop(ResourceId(kResourceTypeAudio, musicSlot->resourceId), musicSlot->soundObj, value == -1); + } +#endif + return acc; } diff --git a/engines/sci/sound/sync.cpp b/engines/sci/sound/sync.cpp index 08abad188bd..4e75dab7251 100644 --- a/engines/sci/sound/sync.cpp +++ b/engines/sci/sound/sync.cpp @@ -25,6 +25,7 @@ #include "sync.h" namespace Sci { + Sync::Sync(ResourceManager *resMan, SegManager *segMan) : _resMan(resMan), _segMan(segMan), diff --git a/engines/sci/sound/sync.h b/engines/sci/sound/sync.h index c80982bff75..4b9e2d1b3c3 100644 --- a/engines/sci/sound/sync.h +++ b/engines/sci/sound/sync.h @@ -38,6 +38,10 @@ class Resource; class ResourceManager; class SegManager; +/** + * Sync class, kDoSync and relevant functions for SCI games. + * Provides AV synchronization for animations. + */ class Sync { SegManager *_segMan; ResourceManager *_resMan; @@ -53,5 +57,5 @@ public: void stop(); }; -} +} // End of namespace Sci #endif