/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "common/system.h" #include "common/substream.h" #include "common/random.h" #include "math/quat.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/iff.h" #include "engines/nancy/state/scene.h" #include "engines/nancy/state/map.h" namespace Nancy { enum SoundType { kSoundTypeDiamondware, kSoundTypeRaw, kSoundTypeOgg }; 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; } uint getAdjustedVolume(uint volume) { if (g_nancy->getGameType() >= kGameTypeNancy3) { return 10 + (volume * 90) / 100; } else { return volume; } } Audio::SeekableAudioStream *SoundManager::makeHISStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse, uint32 overrideSamplesPerSec) { 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 nullptr; } else if (headerID == "Her Interactive Sound") { // Early HIS file if (!readWaveHeader(stream, type, numChannels, samplesPerSec, bitsPerSample, size)) return nullptr; } else if (headerID == "HIS") { stream->seek(4); if (!readHISHeader(stream, type, numChannels, samplesPerSec, bitsPerSample, size)) return nullptr; } 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 nullptr; } if (numChannels == 2) { flags |= Audio::FLAG_STEREO; } else if (numChannels != 1) { warning("Unsupported number of channels %d found in HIS file", numChannels); return nullptr; } // 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(), type == kSoundTypeOgg ? stream->size() : stream->pos() + size, disposeAfterUse); if (type == kSoundTypeRaw || type == kSoundTypeDiamondware) return Audio::makeRawStream(subStream, overrideSamplesPerSec == 0 ? samplesPerSec : overrideSamplesPerSec, flags, DisposeAfterUse::YES); else return Audio::makeVorbisStream(subStream, DisposeAfterUse::YES); } SoundManager::SoundManager() : _shouldRecalculate(false), _mixer(g_system->getMixer()) {} void SoundManager::loadCommonSounds(IFF *boot) { // Persistent sounds that are used across the engine. These originally get loaded inside Logo Common::String chunkNames[] = { "CANT", "CURT", "GLOB", "SLID", "BULS", "BUDE", "BUOK", "TH1", "TH2", }; Common::SeekableReadStream *chunk = nullptr; for (auto const &s : chunkNames) { chunk = boot->getChunkStream(s); if (chunk) { SoundDescription &desc = _commonSounds.getOrCreateVal(s); desc.readNormal(*chunk); g_nancy->_sound->loadSound(desc); _channels[desc.channelID].isPersistent = true; delete chunk; } } // Menu sound is stored differently chunk = boot->getChunkStream("MSND"); // channel 28 if (chunk) { SoundDescription &desc = _commonSounds.getOrCreateVal("MSND"); desc.readMenu(*chunk); g_nancy->_sound->loadSound(desc); _channels[desc.channelID].isPersistent = true; delete chunk; } } SoundManager::~SoundManager() { stopAllSounds(); } void SoundManager::loadSound(const SoundDescription &description, SoundEffectDescription **effectData, bool forceReload) { if (description.name == "NO SOUND") { return; } Channel &existing = _channels[description.channelID]; if (!forceReload && existing.stream != nullptr) { // There's a channel already loaded. Check if we're trying to reload the exact same sound if ( description.name == existing.name && description.numLoops == existing.numLoops && description.playCommands == existing.playCommands) { // When the same sound is already playing at a different volume, adjust to new volume (nancy2 scene 599) if (existing.volume != getAdjustedVolume(description.volume)) { setVolume(description, description.volume); } return; } } if (_mixer->isSoundHandleActive(_channels[description.channelID].handle)) { _mixer->stopHandle(_channels[description.channelID].handle); } Channel &chan = _channels[description.channelID]; delete chan.streamForMixer; chan.stream = nullptr; chan.streamForMixer = nullptr; chan.name = description.name; chan.playCommands = description.playCommands; chan.numLoops = description.numLoops; chan.volume = description.volume; chan.panAnchorFrame = description.panAnchorFrame; chan.isPanning = description.isPanning; if (effectData) { // Channel takes ownership of the effect data delete chan.effectData; chan.effectData = *effectData; *effectData = nullptr; } Common::Path path(description.name + (g_nancy->getGameType() == kGameTypeVampire ? ".dwd" : ".his")); Common::SeekableReadStream *file = SearchMan.createReadStreamForMember(path); if (file) { uint numLoops = chan.numLoops; if (chan.playCommands & kPlayRandomTime) { // We want to add randomized time delays between repeats, which is not doable with // a simple LoopingAudioStream. The delays are added in soundEffectMaintenance(); numLoops = 1; // Decrement the number of loops since we start playing immediately after --chan.numLoops; } chan.stream = makeHISStream(file, DisposeAfterUse::YES, description.samplesPerSec); chan.streamForMixer = Audio::makeLoopingAudioStream(chan.stream, numLoops); } } void SoundManager::playSound(uint16 channelID) { if (channelID >= _channels.size() || _channels[channelID].stream == nullptr || isSoundPlaying(channelID)) return; Channel &chan = _channels[channelID]; chan.stream->seek(0); // Set a minimum volume (10 percent was chosen arbitrarily, but sounds reasonably close) // Fix for nancy3 scene 6112, but NOT a hack; the original engine also set a minimum volume for all sounds chan.volume = getAdjustedVolume(chan.volume); // Init 3D sound if (chan.playCommands & ~kPlaySequential && chan.effectData) { uint16 playCommands = chan.playCommands; if (playCommands & kPlayRandomPosition) { auto *rand = g_nancy->_randomSource; chan.position.set( rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinX, chan.effectData->randomMoveMaxX), rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinY, chan.effectData->randomMoveMaxY), rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinZ, chan.effectData->randomMoveMaxZ)); } if (playCommands == kPlaySequentialPosition) { chan.position.set(chan.effectData->fixedPosX, chan.effectData->fixedPosY, chan.effectData->fixedPosZ); } else if (playCommands == kPlaySequentialFrameAnchor) { // Doesn't seem to be used so we skip implementing it warning("Sound play command kPlaySequentialFrameAnchor not implemented"); } else if (playCommands == kPlayMoveLinear) { chan.position.set(chan.effectData->linearMoveStartX, chan.effectData->linearMoveStartY, chan.effectData->linearMoveStartZ); chan.positionDelta.set(chan.effectData->linearMoveEndX, chan.effectData->linearMoveEndY, chan.effectData->linearMoveEndZ); chan.positionDelta -= chan.position; chan.positionDelta /= chan.effectData->numMoveSteps; chan.nextStepTime = g_nancy->getTotalPlayTime() + chan.effectData->moveStepTime; chan.stepsLeft = chan.effectData->numMoveSteps; } else if (playCommands == kPlayRandomMove) { auto *rand = g_nancy->_randomSource; chan.position.set( rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinX, chan.effectData->randomMoveMaxX), rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinY, chan.effectData->randomMoveMaxY), rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinZ, chan.effectData->randomMoveMaxZ)); chan.positionDelta.set( rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinX, chan.effectData->randomMoveMaxX), rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinY, chan.effectData->randomMoveMaxY), rand->getRandomNumberRngSigned(chan.effectData->randomMoveMinZ, chan.effectData->randomMoveMaxZ)); chan.positionDelta -= chan.position; chan.positionDelta /= chan.effectData->numMoveSteps; chan.nextStepTime = g_nancy->getTotalPlayTime() + chan.effectData->moveStepTime; chan.stepsLeft = chan.effectData->numMoveSteps; } else if (playCommands == kPlayMoveCircular) { chan.position.set(chan.effectData->rotateMoveStartX, chan.effectData->rotateMoveStartY, chan.effectData->rotateMoveStartZ); chan.nextStepTime = g_nancy->getTotalPlayTime() + chan.effectData->moveStepTime; chan.stepsLeft = chan.effectData->numMoveSteps; } } _mixer->playStream( chan.type, &chan.handle, chan.streamForMixer, channelID, (int)chan.volume * 255 / 100, 0, DisposeAfterUse::NO); soundEffectMaintenance(channelID, true); } 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 >= _channels.size()) 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); } void SoundManager::pauseAllSounds(bool pause) { _mixer->pauseAll(pause); } bool SoundManager::isSoundPlaying(uint16 channelID) const { if (channelID >= _channels.size() || !_channels[channelID].stream) return false; const Channel &chan = _channels[channelID]; if (chan.playCommands & kPlayRandomTime) { return _mixer->isSoundHandleActive(chan.handle) || chan.numLoops != 0; } else { return _mixer->isSoundHandleActive(chan.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 >= _channels.size()) return; Channel &chan = _channels[channelID]; if (isSoundPlaying(channelID)) { _mixer->stopHandle(chan.handle); } // Persistent sounds only stop playing but do not get unloaded if (!chan.isPersistent) { chan.name = Common::String(); delete chan.streamForMixer; chan.stream = nullptr; chan.streamForMixer = nullptr; delete chan.effectData; chan.effectData = nullptr; chan.position.set(0, 0, 0); chan.positionDelta.set(0, 0, 0); chan.stepsLeft = 0; chan.nextStepTime = 0; } } void SoundManager::stopSound(const SoundDescription &description) { if (description.name != "NO SOUND") { stopSound(description.channelID); } } void SoundManager::stopSound(const Common::String &chunkName) { stopSound(_commonSounds[chunkName]); } void SoundManager::stopAllSounds() { for (uint i = 0; i < _channels.size(); ++i) { stopSound(i); } } byte SoundManager::getVolume(uint16 channelID) { if (channelID >= _channels.size()) return 0; return _mixer->getChannelVolume(_channels[channelID].handle); } byte SoundManager::getVolume(const SoundDescription &description) { if (description.name != "NO SOUND") { return getVolume(description.channelID); } return 0; } byte SoundManager::getVolume(const Common::String &chunkName) { return getVolume(_commonSounds[chunkName]); } void SoundManager::setVolume(uint16 channelID, uint16 volume) { if (channelID >= _channels.size() || !isSoundPlaying(channelID)) return; _mixer->setChannelVolume(_channels[channelID].handle, getAdjustedVolume(volume) * 255 / 100); } void SoundManager::setVolume(const SoundDescription &description, uint16 volume) { if (description.name != "NO SOUND") { setVolume(description.channelID, volume); } } void SoundManager::setVolume(const Common::String &chunkName, uint16 volume) { setVolume(_commonSounds[chunkName], volume); } uint32 SoundManager::getRate(uint16 channelID) { if (channelID >= _channels.size()) return 0; return _mixer->getChannelRate(_channels[channelID].handle); } uint32 SoundManager::getRate(const SoundDescription &description) { if (description.name != "NO SOUND") { return getRate(description.channelID); } return 0; } uint32 SoundManager::getRate(const Common::String &chunkName) { return getRate(_commonSounds[chunkName]); } uint32 SoundManager::getBaseRate(uint16 channelID) { if (channelID >= _channels.size() || !_channels[channelID].stream) return 0; return _channels[channelID].stream->getRate(); } uint32 SoundManager::getBaseRate(const SoundDescription &description) { if (description.name != "NO SOUND") { return getBaseRate(description.channelID); } return 0; } uint32 SoundManager::getBaseRate(const Common::String &chunkName) { return getBaseRate(_commonSounds[chunkName]); } void SoundManager::setRate(uint16 channelID, uint32 rate) { if (channelID >= _channels.size()) return; _mixer->setChannelRate(_channels[channelID].handle, rate); } void SoundManager::setRate(const SoundDescription &description, uint32 rate) { if (description.name != "NO SOUND") { setRate(description.channelID, rate); } } void SoundManager::setRate(const Common::String &chunkName, uint32 rate) { setRate(_commonSounds[chunkName], rate); } Audio::Timestamp SoundManager::getLength(uint16 channelID) { if (channelID >= _channels.size() || _channels[channelID].stream == nullptr) { return Audio::Timestamp(); } return _channels[channelID].stream->getLength().convertToFramerate(getRate(channelID)); } Audio::Timestamp SoundManager::getLength(const SoundDescription &description) { if (description.name != "NO SOUND") { return getLength(description.channelID); } return Audio::Timestamp(); } Audio::Timestamp SoundManager::getLength(const Common::String &chunkName) { return getLength(_commonSounds[chunkName]); } void SoundManager::recalculateSoundEffects() { _shouldRecalculate = true; _positionLerp = 0; if (g_nancy->getGameType() >= kGameTypeNancy3) { const Nancy::State::Scene::SceneSummary &sceneSummary = NancySceneState.getSceneSummary(); SceneChangeDescription &sceneInfo = NancySceneState.getSceneInfo(); Math::Vector3d rotatedFrontVector = NancySceneState.getSceneInfo().listenerFrontVector; rotatedFrontVector.normalize(); int rotation = sceneInfo.frontVectorFrameID < sceneInfo.frameID ? 360 - (sceneInfo.frameID - sceneInfo.frontVectorFrameID) * sceneSummary.degreesPerRotation : (sceneInfo.frontVectorFrameID - sceneInfo.frameID) * sceneSummary.degreesPerRotation; Math::Quaternion quat = Math::Quaternion::yAxis(rotation); quat.transform(rotatedFrontVector); _orientation = rotatedFrontVector; for (uint i = 0; i < 3; ++i) { if (abs(_orientation.getValue(i)) < Math::epsilon) { _orientation.setValue(i, 0); } } } } void SoundManager::stopAndUnloadSceneSpecificSounds() { byte numSSChans = g_nancy->getStaticData().soundChannelInfo.numSceneSpecificChannels; if (g_nancy->getGameType() == kGameTypeVampire && Nancy::State::Map::hasInstance()) { // Don't stop the map sound in certain scenes uint nextScene = NancySceneState.getNextSceneInfo().sceneID; if (nextScene != 0 && (nextScene < 15 || nextScene > 27)) { stopSound(NancyMapState.getSound()); } } for (uint i = 0; i < numSSChans; ++i) { stopSound(i); } stopSound("MSND"); } void SoundManager::pauseSceneSpecificSounds(bool pause) { byte numSSChans = g_nancy->getStaticData().soundChannelInfo.numSceneSpecificChannels; if (g_nancy->getGameType() == kGameTypeVampire && Nancy::State::Map::hasInstance()) { if (!pause || g_nancy->getState() != NancyState::kMap) { // Stop the map sound in certain scenes uint currentScene = NancySceneState.getSceneInfo().sceneID; if (currentScene == 0 || (currentScene >= 15 && currentScene <= 27)) { g_nancy->_sound->pauseSound(NancyMapState.getSound(), pause); } } } for (uint i = 0; i < numSSChans; ++i) { g_nancy->_sound->pauseSound(i, pause); } } void SoundManager::initSoundChannels() { const SoundChannelInfo &channelInfo = g_nancy->getStaticData().soundChannelInfo; _channels.resize(channelInfo.numChannels); for (const short id : channelInfo.speechChannels) { _channels[id].type = Audio::Mixer::SoundType::kSpeechSoundType; } for (const short id : channelInfo.musicChannels) { _channels[id].type = Audio::Mixer::SoundType::kMusicSoundType; } for (const short id : channelInfo.sfxChannels) { _channels[id].type = Audio::Mixer::SoundType::kSFXSoundType; } } SoundManager::Channel::~Channel() { delete streamForMixer; delete effectData; } void SoundManager::soundEffectMaintenance() { // Interpolate position and rotation when scene has changed to avoid audible chop in sound if (_position != NancySceneState.getSceneSummary().listenerPosition && _positionLerp == 0) { ++_positionLerp; } if (_positionLerp > 1) { ++_positionLerp; if (_positionLerp > 10) { _position = NancySceneState.getSceneSummary().listenerPosition; _positionLerp = 0; } } for (uint i = 0; i < _channels.size(); ++i) { soundEffectMaintenance(i); } _shouldRecalculate = false; } void SoundManager::soundEffectMaintenance(uint16 channelID, bool force) { if (channelID >= _channels.size() || !isSoundPlaying(channelID)) return; uint32 gameTime = g_nancy->getTotalPlayTime(); Channel &chan = _channels[channelID]; // Handle sound effects and 3D sound, which started being used from nancy3. // The original engine used DirectSound 3D, whose effects are only approximated. // In particular, there are some slight but noticeable differences in panning bool hasStepped = force; if (g_nancy->getGameType() >= 3 && chan.effectData) { uint16 playCommands = chan.playCommands; SoundEffectDescription *effectData = chan.effectData; // Handle randomized time delay between repeats if (playCommands & kPlayRandomTime && !_mixer->isSoundHandleActive(chan.handle) && chan.numLoops != 0) { if (chan.nextRepeatTime == 0) { // Channel just stopped playing, add a randomized delay chan.nextRepeatTime = g_nancy->_randomSource->getRandomNumberRng(effectData->minTimeDelay, effectData->maxTimeDelay) + gameTime; } else if (chan.nextRepeatTime < gameTime) { // Delay is over, start playing again _mixer->playStream( chan.type, &chan.handle, chan.stream, channelID, chan.volume * 255 / 100, 0, DisposeAfterUse::NO); --chan.numLoops; chan.nextRepeatTime = 0; } } // Move sound in space if (playCommands & kPlayMoveLinear && chan.stepsLeft && gameTime > chan.nextStepTime) { chan.nextStepTime = gameTime + chan.effectData->moveStepTime; --chan.stepsLeft; hasStepped = true; if (playCommands == kPlayMoveCircular) { // No real uses at least up to nancy5, so this is untested Math::Quaternion quat; switch (chan.effectData->rotateMoveAxis) { case kRotateAroundX: quat = Math::Quaternion::xAxis(360.0 / chan.effectData->numMoveSteps); break; case kRotateAroundY: quat = Math::Quaternion::yAxis(360.0 / chan.effectData->numMoveSteps); break; case kRotateAroundZ: quat = Math::Quaternion::zAxis(360.0 / chan.effectData->numMoveSteps); break; } quat.transform(chan.position); } else { chan.position += chan.positionDelta; } } } // Check if the player has moved OR if the sound itself has moved, OR, if we're still interpolating // Also, make sure we don't accidentally create a Scene state during game startup if (!State::Scene::hasInstance() || (!_shouldRecalculate && !hasStepped && _positionLerp == 0)) { return; } uint16 viewportFrameID = NancySceneState.getSceneInfo().frameID; // Old panning algorithm, used in The Vampire Diaries if (g_nancy->getGameType() <= kGameTypeNancy2 && chan.isPanning) { const State::Scene::SceneSummary &sceneSummary = NancySceneState.getSceneSummary(); switch (sceneSummary.totalViewAngle) { case 180: _mixer->setChannelBalance(chan.handle, CLIP((viewportFrameID - chan.panAnchorFrame) * sceneSummary.degreesPerRotation * 364, -32768, 32767) / 256); break; case 360: { int16 adjustedViewportFrame = viewportFrameID - chan.panAnchorFrame; if (adjustedViewportFrame < 0) { adjustedViewportFrame += sceneSummary.numberOfVideoFrames; } // Divide the virtual space into quarters uint16 q1 = sceneSummary.numberOfVideoFrames / 4; uint16 q2 = sceneSummary.numberOfVideoFrames / 2; uint16 q3 = sceneSummary.numberOfVideoFrames * 3 / 4; float balance; if (adjustedViewportFrame < q1) { balance = (float)adjustedViewportFrame / q1; balance *= 32767; balance = 32768 - balance; } else if (adjustedViewportFrame < q2) { balance = (float)(adjustedViewportFrame - q1) / q1; balance *= 32767; } else if (adjustedViewportFrame < q3) { balance = (float)(adjustedViewportFrame - q2) / q1; balance *= 32767; balance += 32768; } else { balance = (float)(adjustedViewportFrame - q3) / q1; balance *= 32767; balance = 65535 - balance; } // The original engine's algorithm is broken and results in flipped // stereo; the following line fixes this bug balance = 65535 - balance; _mixer->setChannelBalance(chan.handle, (balance - 32768) / 256); break; } default: _mixer->setChannelBalance(chan.handle, 0); break; } } // Panning/volume/rate adjustment used in nancy3 and up. Originally handled by DirectSound 3D if (g_nancy->getGameType() >= 3 && chan.effectData && (chan.playCommands & ~kPlaySequential) & (kPlaySequentialFrameAnchor | kPlayRandomPosition | kPlayMoveLinear)) { // Interpolate position when we've changed scenes Math::Vector3d listenerPos = Math::Vector3d::interpolate(_position, NancySceneState.getSceneSummary().listenerPosition, (float)_positionLerp / 10.0); float dist = listenerPos.getDistanceTo(chan.position); float volume; // Panning is linear, so we calculate it from the difference in degrees Math::Vector3d relativeSoundPos = chan.position - listenerPos; float pan = Math::Vector3d::angle(_orientation, relativeSoundPos.getNormalized()).getDegrees(); if (pan > 90) { pan = 90 - (pan - 90); } pan /= 90; if (Math::Vector3d::crossProduct(_orientation, relativeSoundPos).y() < 0) { pan = -pan; } // Attenuate sound based on distance if (dist <= chan.effectData->minDistance) { volume = 255; } else if (dist >= chan.effectData->maxDistance) { volume = 255.0 / (2 * log2f(chan.effectData->maxDistance - chan.effectData->minDistance + 1)); } else { float dlog = (2 * log2f(dist - chan.effectData->minDistance + 1)); volume = 255.0 / dlog; // Sounds that are closer to the listener shouldn't pan as hard // note: slightly inaccurate, compare the ticking sound in nancy3 scene 4015 pan -= pan / dlog; } // (Non-linearly) interpolate pan as well if (_positionLerp) { float lastPan = _mixer->getChannelBalance(chan.handle) / 127.0; pan = lastPan + (pan - lastPan) * ((float)_positionLerp / 10.0); } // Doppler effect is affected by the velocities of the source and listener, // as projected onto the vector between source and listener Math::Vector3d listenerToSource = chan.position - listenerPos; if (listenerToSource.isZero()) { return; } float projectedListenerVelocity, projectedSrcVelocity; float soundSpeed = 343.0; // It appears the original engine's devs either didn't know or didn't care // what the velocity parameters do, so they used the doubled orientation vector // as listener velocity, and the doubled position vector of the sound as source velocity. // This results in physically incorrect behavior which we replicate projectedListenerVelocity = Math::Vector3d::dotProduct(_orientation * 2, listenerToSource) / listenerToSource.length(); projectedSrcVelocity = -Math::Vector3d::dotProduct(chan.position * 2, listenerToSource) / listenerToSource.length(); // Calculate the final rate of the sound with doppler effect applied uint32 rate = chan.stream->getRate() * (1 + projectedListenerVelocity / soundSpeed) / (1 - projectedSrcVelocity / soundSpeed); _mixer->setChannelVolume(chan.handle, ((byte)volume * chan.volume) / 100); _mixer->setChannelBalance(chan.handle, pan * 127); _mixer->setChannelRate(chan.handle, rate); } } } // End of namespace Nancy