/* 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 "common/system.h" #include "common/substream.h" #include "audio/audiostream.h" #include "audio/decoders/raw.h" #include "audio/decoders/vorbis.h" #include "engines/nancy/nancy.h" #include "engines/nancy/sound.h" #include "engines/nancy/state/scene.h" namespace Nancy { enum SoundType { kSoundTypeDiamondware, kSoundTypeRaw, kSoundTypeOgg }; // Table valid for vampire diaries and nancy1, could be (and probably is) different between games static const Audio::Mixer::SoundType channelSoundTypes[] = { Audio::Mixer::kMusicSoundType, // channel 0 Audio::Mixer::kMusicSoundType, Audio::Mixer::kMusicSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, // 5 Audio::Mixer::kSFXSoundType, Audio::Mixer::kSpeechSoundType, Audio::Mixer::kSpeechSoundType, Audio::Mixer::kPlainSoundType, Audio::Mixer::kPlainSoundType, // 10 Audio::Mixer::kPlainSoundType, Audio::Mixer::kPlainSoundType, Audio::Mixer::kPlainSoundType, Audio::Mixer::kPlainSoundType, Audio::Mixer::kPlainSoundType, // 15 Audio::Mixer::kPlainSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kMusicSoundType, Audio::Mixer::kSFXSoundType, // 20 Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, Audio::Mixer::kSFXSoundType, // 25 Audio::Mixer::kSFXSoundType, Audio::Mixer::kMusicSoundType, Audio::Mixer::kMusicSoundType, Audio::Mixer::kMusicSoundType, Audio::Mixer::kSpeechSoundType, // 30 Audio::Mixer::kSFXSoundType }; bool readDiamondwareHeader(Common::SeekableReadStream *stream, SoundType &type, uint16 &numChannels, uint32 &samplesPerSec, uint16 &bitsPerSample, uint32 &size) { stream->skip(2); if (stream->readByte() != 1 || stream->readByte() > 1) { // Version, up to 1.1 is supported return false; } stream->skip(5); // sound id, reserved if (stream->readByte() != 0) { // Compression type, only uncompressed (0) is supported return false; } samplesPerSec = stream->readUint16LE(); numChannels = stream->readByte(); bitsPerSample = stream->readByte(); stream->skip(2); // Absolute value of largest sample in file size = stream->readUint32LE(); stream->skip(4); // Number of samples uint dataOffset = stream->readUint16LE(); stream->seek(dataOffset); type = kSoundTypeDiamondware; return true; } bool readWaveHeader(Common::SeekableReadStream *stream, SoundType &type, uint16 &numChannels, uint32 &samplesPerSec, uint16 &bitsPerSample, uint32 &size) { // The earliest HIS files are just WAVE files with the first 22 bytes of // the file overwritten with a string, so most of this is copied from the // standard WAVE decoder numChannels = stream->readUint16LE(); samplesPerSec = stream->readUint32LE(); stream->skip(6); bitsPerSample = stream->readUint16LE(); char buf[4 + 1]; stream->read(buf, 4); buf[4] = 0; if (Common::String(buf) != "data") { warning("Data chunk not found in HIS file"); return false; } size = stream->readUint32LE(); if (stream->eos() || stream->err()) { warning("Error reading HIS file"); return false; } type = kSoundTypeRaw; return true; } bool readHISHeader(Common::SeekableReadStream *stream, SoundType &type, uint16 &numChannels, uint32 &samplesPerSec, uint16 &bitsPerSample, uint32 &size) { uint32 ver; ver = stream->readUint16LE() << 16; ver |= stream->readUint16LE(); bool hasType = false; switch (ver) { case 0x00010000: break; case 0x00020000: hasType = true; break; default: warning("Unsupported version %d.%d found in HIS file", ver >> 16, ver & 0xffff); return false; } // Same data as Wave fmt chunk stream->skip(2); // AudioFormat numChannels = stream->readUint16LE(); samplesPerSec = stream->readUint32LE(); stream->skip(6); // ByteRate and BlockAlign bitsPerSample = stream->readUint16LE(); size = stream->readUint32LE(); if (hasType) { uint16 tp = stream->readUint16LE(); switch (tp) { case 1: type = kSoundTypeRaw; break; case 2: type = kSoundTypeOgg; break; default: warning("Unsupported sound type %d found in HIS file", tp); return false; } } else type = kSoundTypeRaw; if (stream->eos() || stream->err()) { warning("Error reading HIS file"); return false; } return true; } Audio::SeekableAudioStream *SoundManager::makeHISStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) { char buf[22]; stream->read(buf, 22); buf[21] = 0; Common::String headerID(buf); uint16 numChannels = 0, bitsPerSample = 0; uint32 samplesPerSec = 0, size = 0; SoundType type = kSoundTypeRaw; if (headerID == "DiamondWare Digitized") { if (!readDiamondwareHeader(stream, type, numChannels, samplesPerSec, bitsPerSample, size)) return 0; } else if (headerID == "Her Interactive Sound") { // Early HIS file if (!readWaveHeader(stream, type, numChannels, samplesPerSec, bitsPerSample, size)) return 0; } else if (headerID == "HIS") { stream->seek(4); if (!readHISHeader(stream, type, numChannels, samplesPerSec, bitsPerSample, size)) return 0; } byte flags = 0; if (type == kSoundTypeRaw || type == kSoundTypeDiamondware) { if (bitsPerSample == 8) { // 8 bit data is unsigned in HIS files and signed in DWD files flags |= (type == kSoundTypeRaw ? Audio::FLAG_UNSIGNED : Audio::FLAG_LITTLE_ENDIAN); } else if (bitsPerSample == 16) { // 16 bit data is signed little endian flags |= (Audio::FLAG_16BITS | Audio::FLAG_LITTLE_ENDIAN); } else { warning("Unsupported bitsPerSample %d found in HIS file", bitsPerSample); return 0; } if (numChannels == 2) { flags |= Audio::FLAG_STEREO; } else if (numChannels != 1) { warning("Unsupported number of channels %d found in HIS file", numChannels); return 0; } // Raw PCM, make sure the last packet is complete uint sampleSize = (flags & Audio::FLAG_16BITS ? 2 : 1) * (flags & Audio::FLAG_STEREO ? 2 : 1); if (size % sampleSize != 0) { warning("Trying to play an %s file with an incomplete PCM packet", type == kSoundTypeDiamondware ? "DWD" : "HIS"); size &= ~(sampleSize - 1); } } Common::SeekableSubReadStream *subStream = new Common::SeekableSubReadStream(stream, stream->pos(), stream->pos() + size, disposeAfterUse); if (type == kSoundTypeRaw || type == kSoundTypeDiamondware) return Audio::makeRawStream(subStream, samplesPerSec, flags, DisposeAfterUse::YES); else return Audio::makeVorbisStream(subStream, DisposeAfterUse::YES); } SoundManager::SoundManager() { _mixer = g_system->getMixer(); initSoundChannels(); } void SoundManager::loadCommonSounds() { // Persistent sounds that are used across the engine. These originally get loaded inside Logo Common::String chunkNames[] = { "CANT", // channel 17 "CURT", // channel 18 "GLOB", // channel 20 "BULS", // channel 22 "BUDE", // channel 23 "BUOK", // channel 24 }; Common::SeekableReadStream *chunk = nullptr; for (auto const &s : chunkNames) { chunk = g_nancy->getBootChunkStream(s); if (chunk) { SoundDescription &desc = _commonSounds.getOrCreateVal(s); desc.read(*chunk, SoundDescription::kNormal); g_nancy->_sound->loadSound(desc); } } // Menu sound is special since it's stored differently and can be // unloaded and loaded again chunk = g_nancy->getBootChunkStream("MSND"); // channel 28 if (chunk) { SoundDescription &desc = _commonSounds.getOrCreateVal("MSND"); desc.read(*chunk, SoundDescription::kMenu); } } SoundManager::~SoundManager() { stopAllSounds(); } void SoundManager::loadSound(const SoundDescription &description, bool panning) { if (description.name == "NO SOUND") { return; } if (_mixer->isSoundHandleActive(_channels[description.channelID].handle)) { _mixer->stopHandle(_channels[description.channelID].handle); } Channel &chan = _channels[description.channelID]; delete chan.stream; chan.stream = nullptr; chan.name = description.name; chan.numLoops = description.numLoops; chan.volume = description.volume; chan.panAnchorFrame = description.panAnchorFrame; chan.isPanning = panning; Common::SeekableReadStream *file = SearchMan.createReadStreamForMember(description.name + (g_nancy->getGameType() == kGameTypeVampire ? ".dwd" : ".his")); if (file) { _channels[description.channelID].stream = makeHISStream(file, DisposeAfterUse::YES); } } void SoundManager::playSound(uint16 channelID) { if (channelID > 31 || _channels[channelID].stream == nullptr) return; Channel &chan = _channels[channelID]; chan.stream->seek(0); _mixer->playStream( chan.type, &chan.handle, Audio::makeLoopingAudioStream(chan.stream, chan.numLoops), channelID, chan.volume * 255 / 100, 0, DisposeAfterUse::NO); } void SoundManager::playSound(const SoundDescription &description) { if (description.name != "NO SOUND") { playSound(description.channelID); } } void SoundManager::playSound(const Common::String &chunkName) { const SoundDescription &desc = _commonSounds[chunkName]; if (!isSoundPlaying(desc)) { loadSound(desc); } playSound(desc); } void SoundManager::pauseSound(uint16 channelID, bool pause) { if (channelID > 31) return; if (isSoundPlaying(channelID)) { g_system->getMixer()->pauseHandle(_channels[channelID].handle, pause); } } void SoundManager::pauseSound(const SoundDescription &description, bool pause) { if (description.name != "NO SOUND") { pauseSound(description.channelID, pause); } } void SoundManager::pauseSound(const Common::String &chunkName, bool pause) { pauseSound(_commonSounds[chunkName], pause); } bool SoundManager::isSoundPlaying(uint16 channelID) const { if (channelID > 31) return false; return _mixer->isSoundHandleActive(_channels[channelID].handle); } bool SoundManager::isSoundPlaying(const SoundDescription &description) const { if (description.name == "NO SOUND") { return false; } else { return isSoundPlaying(description.channelID); } } bool SoundManager::isSoundPlaying(const Common::String &chunkName) const { return isSoundPlaying(_commonSounds[chunkName]); } void SoundManager::stopSound(uint16 channelID) { if (channelID > 31) return; Channel &chan = _channels[channelID]; if (isSoundPlaying(channelID)) { _mixer->stopHandle(chan.handle); } chan.name = Common::String(); delete chan.stream; chan.stream = nullptr; } void SoundManager::stopSound(const SoundDescription &description) { if (description.name != "NO SOUND") { stopSound(description.channelID); } } void SoundManager::stopSound(const Common::String &chunkName) { stopSound(_commonSounds[chunkName]); } // Returns whether the exception was skipped void SoundManager::stopAllSounds() { for (uint i = 0; i < 31; ++i) { stopSound(i); } } void SoundManager::calculatePanForAllSounds() { uint16 viewportFrameID = NancySceneState.getSceneInfo().frameID; const State::Scene::SceneSummary &sceneSummary = NancySceneState.getSceneSummary(); for (uint i = 0; i < 31; ++i) { Channel &chan = _channels[i]; if (chan.isPanning) { switch (sceneSummary.totalViewAngle) { case 180: _mixer->setChannelBalance(chan.handle, CLIP((viewportFrameID - chan.panAnchorFrame) * sceneSummary.soundPanPerFrame * 364, -32768, 32767) / 256); break; case 360: // TODO _mixer->setChannelBalance(chan.handle, 0); break; default: _mixer->setChannelBalance(chan.handle, 0); break; } } } } void SoundManager::stopAndUnloadSpecificSounds() { // TODO missing if for (uint i = 0; i < 10; ++i) { stopSound(i); } stopSound(_commonSounds["MSND"]); } void SoundManager::initSoundChannels() { // Channel types are hardcoded in the original engine for (uint i = 0; i < 31; ++i) { _channels[i].type = channelSoundTypes[i]; } } } // End of namespace Nancy