diff options
author | Colin Snover | 2016-03-18 22:55:56 -0500 |
---|---|---|
committer | Colin Snover | 2016-06-20 21:02:21 -0500 |
commit | 46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5 (patch) | |
tree | 09a584e4930b9b3e6b9235680cac31028956eccd | |
parent | 5d3385750ddf68f5347bf51f005c86a8e70283e2 (diff) | |
download | scummvm-rg350-46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5.tar.gz scummvm-rg350-46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5.tar.bz2 scummvm-rg350-46551fd4b53fc8bf4bbdd3a59aeed56f6f9b53e5.zip |
SCI32: Rewrite digital audio engine
This provides a complete implementation of kDoAudio through
SCI2.1mid, plus partial implementation of SCI3 features.
Digital audio calls shunted through kDoSound have also been
updated to go through the SCI32 audio mixer, though these shunts
are a bit hacky because the ScummVM implementation of kDoSound
does not currently match how SSCI kDoSound is designed.
It is probably possible in the future to just replace the SCI1.1
audio code (audio.cpp) with the new SCI32 code, since the major
differences seem to be that (1) SCI1.1 only supported one digital
audio playback channel (this is configurable already), (2) it
had extra commands for CD audio playback and queued sample
playback.
-rw-r--r-- | engines/sci/engine/kernel.h | 20 | ||||
-rw-r--r-- | engines/sci/engine/kernel_tables.h | 68 | ||||
-rw-r--r-- | engines/sci/engine/ksound.cpp | 169 | ||||
-rw-r--r-- | engines/sci/module.mk | 2 | ||||
-rw-r--r-- | engines/sci/resource.cpp | 19 | ||||
-rw-r--r-- | engines/sci/resource.h | 13 | ||||
-rw-r--r-- | engines/sci/resource_audio.cpp | 2 | ||||
-rw-r--r-- | engines/sci/sci.cpp | 17 | ||||
-rw-r--r-- | engines/sci/sci.h | 2 | ||||
-rw-r--r-- | engines/sci/sound/audio.cpp | 8 | ||||
-rw-r--r-- | engines/sci/sound/audio32.cpp | 961 | ||||
-rw-r--r-- | engines/sci/sound/audio32.h | 566 | ||||
-rw-r--r-- | engines/sci/sound/decoders/sol.cpp | 272 | ||||
-rw-r--r-- | engines/sci/sound/decoders/sol.h | 89 | ||||
-rw-r--r-- | engines/sci/sound/music.cpp | 74 | ||||
-rw-r--r-- | engines/sci/sound/music.h | 4 | ||||
-rw-r--r-- | engines/sci/sound/soundcmd.cpp | 104 | ||||
-rw-r--r-- | engines/sci/sound/sync.cpp | 1 | ||||
-rw-r--r-- | engines/sci/sound/sync.h | 6 |
19 files changed, 2341 insertions, 56 deletions
diff --git a/engines/sci/engine/kernel.h b/engines/sci/engine/kernel.h index 62566a74b2..d88dc75dab 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 0ede307e6b..a61213edb7 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 2b22d6867b..301e4c1b08 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<int16>(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<int16>(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 c35d0b51dd..69ddf00183 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 9f977aaefd..2a83a57227 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 ef474d97c2..f70bf48bd4 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 5717a09121..5aeff81b77 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 8c23d322e7..6244c43764 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 ff414727fa..56ee2f4403 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 71b081049e..4fb9a58003 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<int32>(s, 0, 255); diff --git a/engines/sci/sound/audio32.cpp b/engines/sci/sound/audio32.cpp new file mode 100644 index 0000000000..56b10c55fe --- /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 0000000000..59aba66e2c --- /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<AudioChannel> _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 0000000000..280b24fd3a --- /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<int16>(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<byte>(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<bool STEREO, bool S16BIT> +SOLStream<STEREO, S16BIT>::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 STEREO, bool S16BIT> +bool SOLStream<STEREO, S16BIT>::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 <bool STEREO, bool S16BIT> +Audio::Timestamp SOLStream<STEREO, S16BIT>::getLength() const override { + return _length; +} + +template <bool STEREO, bool S16BIT> +int SOLStream<STEREO, S16BIT>::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 STEREO, bool S16BIT> +bool SOLStream<STEREO, S16BIT>::isStereo() const override { + return STEREO; +} + +template <bool STEREO, bool S16BIT> +int SOLStream<STEREO, S16BIT>::getRate() const override { + return _sampleRate; +} + +template <bool STEREO, bool S16BIT> +bool SOLStream<STEREO, S16BIT>::endOfData() const override { + return _stream->eos() || _stream->pos() >= _dataOffset + _rawDataSize; +} + +template <bool STEREO, bool S16BIT> +bool SOLStream<STEREO, S16BIT>::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<true, true>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else if (flags & kStereo) { + return new SOLStream<true, false>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else if (flags & k16Bit) { + return new SOLStream<false, true>(new Common::SeekableSubReadStream(stream, initialPosition, initialPosition + dataSize, disposeAfterUse), disposeAfterUse, headerSize, sampleRate, dataSize); + } else { + return new SOLStream<false, false>(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<true, true>(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else if (flags & kStereo) { + return new SOLStream<true, false>(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else if (flags & k16Bit) { + return new SOLStream<false, true>(dataStream, disposeAfterUse, 0, sampleRate, dataSize); + } else { + return new SOLStream<false, false>(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 0000000000..1046d0b213 --- /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 <bool STEREO, bool S16BIT> +class SOLStream : public Audio::SeekableAudioStream { +private: + /** + * Read stream containing possibly-compressed SOL audio. + */ + Common::DisposablePtr<Common::SeekableReadStream> _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 5a11ac386a..3f34ecc2f8 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 047f63b3b7..3a6de81c49 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 efa4735bf4..7d2ab32c75 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<int16>(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<int>(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 08abad188b..4e75dab725 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 c80982bff7..4b9e2d1b3c 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 |