diff --git a/CHANGELOG.md b/CHANGELOG.md index 681c3bb307..a3f7fb1bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Feature #5492: Let rain and snow collide with statics Feature #6447: Add LOD support to Object Paging Feature #6491: Add support for Qt6 + Feature #6556: Lua API for sounds Feature #6726: Lua API for creating new objects Feature #6922: Improve launcher appearance Feature #6933: Support high-resolution cursor textures diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index e6c53c73bc..3df00f1be0 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -61,7 +61,7 @@ add_openmw_dir (mwscript add_openmw_dir (mwlua luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings - camerabindings uibindings inputbindings nearbybindings postprocessingbindings stats debugbindings + camerabindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal worker magicbindings ) diff --git a/apps/openmw/mwbase/soundmanager.hpp b/apps/openmw/mwbase/soundmanager.hpp index 5939318856..3dd9cd3b33 100644 --- a/apps/openmw/mwbase/soundmanager.hpp +++ b/apps/openmw/mwbase/soundmanager.hpp @@ -52,6 +52,7 @@ namespace MWSound NoScaling = 1 << 4, /* Don't scale audio with simulation time */ NoEnvNoScaling = NoEnv | NoScaling, LoopNoEnv = Loop | NoEnv, + LoopNoEnvNoScaling = Loop | NoEnv | NoScaling, LoopRemoveAtDistance = Loop | RemoveAtDistance }; @@ -117,11 +118,11 @@ namespace MWBase virtual void say(const MWWorld::ConstPtr& reference, const std::string& filename) = 0; ///< Make an actor say some text. - /// \param filename name of a sound file in "Sound/" in the data directory. + /// \param filename name of a sound file in the VFS virtual void say(const std::string& filename) = 0; ///< Say some text, without an actor ref - /// \param filename name of a sound file in "Sound/" in the data directory. + /// \param filename name of a sound file in the VFS virtual bool sayActive(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const = 0; ///< Is actor not speaking? @@ -155,6 +156,12 @@ namespace MWBase ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. + virtual Sound* playSound(std::string_view fileName, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a sound, independently of 3D-position + ///< @param offset Number of seconds into the sound to start playback. + virtual Sound* playSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId, float volume, float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) = 0; @@ -162,6 +169,13 @@ namespace MWBase ///< Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. + virtual Sound* playSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName, float volume, + float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) + = 0; + ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless + ///< Play_NoTrack is specified. + ///< @param offset Number of seconds into the sound to start playback. + virtual Sound* playSound3D(const osg::Vec3f& initialPos, const ESM::RefId& soundId, float volume, float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) = 0; @@ -172,7 +186,10 @@ namespace MWBase ///< Stop the given sound from playing virtual void stopSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) = 0; - ///< Stop the given object from playing the given sound, + ///< Stop the given object from playing the given sound. + + virtual void stopSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName) = 0; + ///< Stop the given object from playing the given sound. virtual void stopSound3D(const MWWorld::ConstPtr& reference) = 0; ///< Stop the given object from playing all sounds. @@ -190,6 +207,10 @@ namespace MWBase ///< Is the given sound currently playing on the given object? /// If you want to check if sound played with playSound is playing, use empty Ptr + virtual bool getSoundPlaying(const MWWorld::ConstPtr& reference, std::string_view fileName) const = 0; + ///< Is the given sound currently playing on the given object? + /// If you want to check if sound played with playSound is playing, use empty Ptr + virtual void pauseSounds(MWSound::BlockerType blocker, int types = int(Type::Mask)) = 0; ///< Pauses all currently playing sounds, including music. diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp index 28611da4be..d040f8eef5 100644 --- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp +++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp @@ -23,6 +23,8 @@ #include #include +#include + #include #include "../mwbase/environment.hpp" @@ -650,7 +652,7 @@ namespace MWDialogue if (Settings::gui().mSubtitles) winMgr->messageBox(info->mResponse); if (!info->mSound.empty()) - sndMgr->say(actor, info->mSound); + sndMgr->say(actor, Misc::ResourceHelpers::correctSoundPath(info->mSound)); if (!info->mResultScript.empty()) executeScript(info->mResultScript, actor); } diff --git a/apps/openmw/mwgui/charactercreation.cpp b/apps/openmw/mwgui/charactercreation.cpp index 80e33383c2..5aaa1e7330 100644 --- a/apps/openmw/mwgui/charactercreation.cpp +++ b/apps/openmw/mwgui/charactercreation.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include "../mwbase/environment.hpp" @@ -656,7 +657,7 @@ namespace MWGui += MyGUI::newDelegate(this, &CharacterCreation::onClassQuestionChosen); mGenerateClassQuestionDialog->setVisible(true); - MWBase::Environment::get().getSoundManager()->say(step.mSound); + MWBase::Environment::get().getSoundManager()->say(Misc::ResourceHelpers::correctSoundPath(step.mSound)); } void CharacterCreation::selectGeneratedClass() diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 28bf9f7999..ec0f09b59b 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -39,6 +39,7 @@ #include "nearbybindings.hpp" #include "objectbindings.hpp" #include "postprocessingbindings.hpp" +#include "soundbindings.hpp" #include "types/types.hpp" #include "uibindings.hpp" @@ -120,7 +121,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 42; + api["API_REVISION"] = 43; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); MWBase::Environment::get().getStateManager()->requestQuit(); @@ -130,6 +131,7 @@ namespace MWLua { std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) }); }; api["contentFiles"] = initContentFilesBindings(lua->sol()); + api["sound"] = initCoreSoundBindings(context); api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); for (size_t i = 0; i < contentList.size(); ++i) @@ -331,6 +333,7 @@ namespace MWLua std::map initPlayerPackages(const Context& context) { return { + { "openmw.ambient", initAmbientPackage(context) }, { "openmw.camera", initCameraPackage(context.mLua->sol()) }, { "openmw.debug", initDebugPackage(context) }, { "openmw.input", initInputPackage(context) }, diff --git a/apps/openmw/mwlua/soundbindings.cpp b/apps/openmw/mwlua/soundbindings.cpp new file mode 100644 index 0000000000..7f3bdc0ecf --- /dev/null +++ b/apps/openmw/mwlua/soundbindings.cpp @@ -0,0 +1,198 @@ +#include "soundbindings.hpp" +#include "luabindings.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/soundmanager.hpp" +#include "../mwbase/windowmanager.hpp" + +#include +#include +#include + +#include "../mwworld/esmstore.hpp" + +#include "luamanagerimp.hpp" + +namespace MWLua +{ + struct PlaySoundArgs + { + bool mScale = true; + bool mLoop = false; + float mVolume = 1.f; + float mPitch = 1.f; + float mTimeOffset = 0.f; + }; + + PlaySoundArgs getPlaySoundArgs(const sol::optional& options) + { + PlaySoundArgs args; + + if (options.has_value()) + { + args.mLoop = options->get_or("loop", false); + args.mVolume = options->get_or("volume", 1.f); + args.mPitch = options->get_or("pitch", 1.f); + args.mTimeOffset = options->get_or("timeOffset", 0.f); + args.mScale = options->get_or("scale", true); + } + return args; + } + + MWSound::PlayMode getPlayMode(const PlaySoundArgs& args, bool is3D) + { + if (is3D) + { + if (args.mLoop) + return MWSound::PlayMode::LoopRemoveAtDistance; + return MWSound::PlayMode::Normal; + } + + if (args.mLoop && !args.mScale) + return MWSound::PlayMode::LoopNoEnvNoScaling; + else if (args.mLoop) + return MWSound::PlayMode::LoopNoEnv; + else if (!args.mScale) + return MWSound::PlayMode::NoEnvNoScaling; + return MWSound::PlayMode::NoEnv; + } + + sol::table initAmbientPackage(const Context& context) + { + sol::table api(context.mLua->sol(), sol::create); + + api["playSound"] = [](std::string_view soundId, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, false); + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + + MWBase::Environment::get().getSoundManager()->playSound( + sound, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + api["playSoundFile"] = [](std::string_view fileName, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, false); + + MWBase::Environment::get().getSoundManager()->playSound( + fileName, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + + api["stopSound"] = [](std::string_view soundId) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + MWBase::Environment::get().getSoundManager()->stopSound3D(MWWorld::Ptr(), sound); + }; + api["stopSoundFile"] = [](std::string_view fileName) { + MWBase::Environment::get().getSoundManager()->stopSound3D(MWWorld::Ptr(), fileName); + }; + + api["isSoundPlaying"] = [](std::string_view soundId) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), sound); + }; + api["isSoundFilePlaying"] = [](std::string_view fileName) { + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), fileName); + }; + + return LuaUtil::makeReadOnly(api); + } + + sol::table initCoreSoundBindings(const Context& context) + { + sol::state_view& lua = context.mLua->sol(); + sol::table api(lua, sol::create); + + api["playSound3d"] + = [](std::string_view soundId, const Object& object, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, true); + + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + + MWBase::Environment::get().getSoundManager()->playSound3D( + object.ptr(), sound, args.mVolume, args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + api["playSoundFile3d"] + = [](std::string_view fileName, const Object& object, const sol::optional& options) { + auto args = getPlaySoundArgs(options); + auto playMode = getPlayMode(args, true); + + MWBase::Environment::get().getSoundManager()->playSound3D(object.ptr(), fileName, args.mVolume, + args.mPitch, MWSound::Type::Sfx, playMode, args.mTimeOffset); + }; + + api["stopSound3d"] = [](std::string_view soundId, const Object& object) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + MWBase::Environment::get().getSoundManager()->stopSound3D(object.ptr(), sound); + }; + api["stopSoundFile3d"] = [](std::string_view fileName, const Object& object) { + MWBase::Environment::get().getSoundManager()->stopSound3D(object.ptr(), fileName); + }; + + api["isSoundPlaying"] = [](std::string_view soundId, const Object& object) { + ESM::RefId sound = ESM::RefId::deserializeText(soundId); + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(object.ptr(), sound); + }; + api["isSoundFilePlaying"] = [](std::string_view fileName, const Object& object) { + return MWBase::Environment::get().getSoundManager()->getSoundPlaying(object.ptr(), fileName); + }; + + api["say"] = sol::overload( + [luaManager = context.mLuaManager]( + std::string_view fileName, const Object& object, sol::optional text) { + MWBase::Environment::get().getSoundManager()->say(object.ptr(), std::string(fileName)); + if (text) + luaManager->addUIMessage(*text); + }, + [luaManager = context.mLuaManager](std::string_view fileName, sol::optional text) { + MWBase::Environment::get().getSoundManager()->say(std::string(fileName)); + if (text) + luaManager->addUIMessage(*text); + }); + api["stopSay"] = sol::overload( + [](const Object& object) { + const MWWorld::Ptr& objPtr = object.ptr(); + MWBase::Environment::get().getSoundManager()->stopSay(objPtr); + }, + []() { MWBase::Environment::get().getSoundManager()->stopSay(MWWorld::ConstPtr()); }); + api["isSayActive"] = sol::overload( + [](const Object& object) { + const MWWorld::Ptr& objPtr = object.ptr(); + return MWBase::Environment::get().getSoundManager()->sayActive(objPtr); + }, + []() { return MWBase::Environment::get().getSoundManager()->sayActive(MWWorld::ConstPtr()); }); + + // Sound store + using SoundStore = MWWorld::Store; + const SoundStore* soundStore = &MWBase::Environment::get().getWorld()->getStore().get(); + sol::usertype soundStoreT = lua.new_usertype("ESM3_SoundStore"); + soundStoreT[sol::meta_function::to_string] + = [](const SoundStore& store) { return "ESM3_SoundStore{" + std::to_string(store.getSize()) + " sounds}"; }; + soundStoreT[sol::meta_function::length] = [](const SoundStore& store) { return store.getSize(); }; + soundStoreT[sol::meta_function::index] = sol::overload( + [](const SoundStore& store, size_t index) -> const ESM::Sound* { return store.at(index - 1); }, + [](const SoundStore& store, std::string_view soundId) -> const ESM::Sound* { + return store.find(ESM::RefId::deserializeText(soundId)); + }); + soundStoreT[sol::meta_function::pairs] = lua["ipairsForArray"].template get(); + soundStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get(); + + api["sounds"] = soundStore; + + // Sound record + auto soundT = lua.new_usertype("ESM3_Sound"); + soundT[sol::meta_function::to_string] + = [](const ESM::Sound& rec) -> std::string { return "ESM3_Sound[" + rec.mId.toDebugString() + "]"; }; + soundT["id"] = sol::readonly_property([](const ESM::Sound& rec) { return rec.mId.serializeText(); }); + soundT["volume"] + = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mVolume; }); + soundT["minRange"] + = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMinRange; }); + soundT["maxRange"] + = sol::readonly_property([](const ESM::Sound& rec) -> unsigned char { return rec.mData.mMaxRange; }); + soundT["fileName"] = sol::readonly_property([](const ESM::Sound& rec) -> std::string { + return VFS::Path::normalizeFilename(Misc::ResourceHelpers::correctSoundPath(rec.mSound)); + }); + + return LuaUtil::makeReadOnly(api); + } +} diff --git a/apps/openmw/mwlua/soundbindings.hpp b/apps/openmw/mwlua/soundbindings.hpp new file mode 100644 index 0000000000..333ed898c4 --- /dev/null +++ b/apps/openmw/mwlua/soundbindings.hpp @@ -0,0 +1,15 @@ +#ifndef MWLUA_SOUNDBINDINGS_H +#define MWLUA_SOUNDBINDINGS_H + +#include + +#include "context.hpp" + +namespace MWLua +{ + sol::table initCoreSoundBindings(const Context&); + + sol::table initAmbientPackage(const Context& context); +} + +#endif // MWLUA_SOUNDBINDINGS_H diff --git a/apps/openmw/mwscript/soundextensions.cpp b/apps/openmw/mwscript/soundextensions.cpp index 918a0c0001..f1ac2a7a08 100644 --- a/apps/openmw/mwscript/soundextensions.cpp +++ b/apps/openmw/mwscript/soundextensions.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "../mwbase/environment.hpp" @@ -38,7 +39,7 @@ namespace MWScript std::string_view text = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); - MWBase::Environment::get().getSoundManager()->say(ptr, file); + MWBase::Environment::get().getSoundManager()->say(ptr, Misc::ResourceHelpers::correctSoundPath(file)); if (Settings::gui().mSubtitles) context.messageBox(text); diff --git a/apps/openmw/mwsound/sound_buffer.cpp b/apps/openmw/mwsound/sound_buffer.cpp index a93cbdb6c8..8aaaca1634 100644 --- a/apps/openmw/mwsound/sound_buffer.cpp +++ b/apps/openmw/mwsound/sound_buffer.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -61,6 +62,35 @@ namespace MWSound return nullptr; } + Sound_Buffer* SoundBufferPool::lookup(std::string_view fileName) const + { + auto soundId = ESM::RefId::stringRefId(fileName); + return lookup(soundId); + } + + Sound_Buffer* SoundBufferPool::loadSfx(Sound_Buffer* sfx) + { + if (sfx->getHandle() != nullptr) + return sfx; + + auto [handle, size] = mOutput->loadSound(sfx->getResourceName()); + if (handle == nullptr) + return {}; + + sfx->mHandle = handle; + + mBufferCacheSize += size; + if (mBufferCacheSize > mBufferCacheMax) + { + unloadUnused(); + if (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMax) + Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; + } + mUnusedBuffers.push_front(sfx); + + return sfx; + } + Sound_Buffer* SoundBufferPool::load(const ESM::RefId& soundId) { if (mBufferNameMap.empty()) @@ -81,25 +111,23 @@ namespace MWSound sfx = insertSound(soundId, *sound); } - if (sfx->getHandle() == nullptr) + return loadSfx(sfx); + } + + Sound_Buffer* SoundBufferPool::load(std::string_view fileName) + { + auto soundId = ESM::RefId::stringRefId(fileName); + + Sound_Buffer* sfx; + const auto it = mBufferNameMap.find(soundId); + if (it != mBufferNameMap.end()) + sfx = it->second; + else { - auto [handle, size] = mOutput->loadSound(sfx->getResourceName()); - if (handle == nullptr) - return {}; - - sfx->mHandle = handle; - - mBufferCacheSize += size; - if (mBufferCacheSize > mBufferCacheMax) - { - unloadUnused(); - if (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMax) - Log(Debug::Warning) << "No unused sound buffers to free, using " << mBufferCacheSize << " bytes!"; - } - mUnusedBuffers.push_front(sfx); + sfx = insertSound(fileName); } - return sfx; + return loadSfx(sfx); } void SoundBufferPool::clear() @@ -113,6 +141,24 @@ namespace MWSound mUnusedBuffers.clear(); } + Sound_Buffer* SoundBufferPool::insertSound(std::string_view fileName) + { + static const AudioParams audioParams + = makeAudioParams(MWBase::Environment::get().getESMStore()->get()); + + float volume = 1.f; + float min = std::max(audioParams.mAudioDefaultMinDistance * audioParams.mAudioMinDistanceMult, 1.f); + float max = std::max(min, audioParams.mAudioDefaultMaxDistance * audioParams.mAudioMaxDistanceMult); + + min = std::max(min, 1.0f); + max = std::max(min, max); + + Sound_Buffer& sfx = mSoundBuffers.emplace_back(fileName, volume, min, max); + + mBufferNameMap.emplace(ESM::RefId::stringRefId(fileName), &sfx); + return &sfx; + } + Sound_Buffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM::Sound& sound) { static const AudioParams audioParams @@ -132,7 +178,8 @@ namespace MWSound min = std::max(min, 1.0f); max = std::max(min, max); - Sound_Buffer& sfx = mSoundBuffers.emplace_back("Sound/" + sound.mSound, volume, min, max); + Sound_Buffer& sfx + = mSoundBuffers.emplace_back(Misc::ResourceHelpers::correctSoundPath(sound.mSound), volume, min, max); VFS::Path::normalizeFilenameInPlace(sfx.mResourceName); mBufferNameMap.emplace(soundId, &sfx); diff --git a/apps/openmw/mwsound/sound_buffer.hpp b/apps/openmw/mwsound/sound_buffer.hpp index a56bcc04ff..bb7c976352 100644 --- a/apps/openmw/mwsound/sound_buffer.hpp +++ b/apps/openmw/mwsound/sound_buffer.hpp @@ -69,10 +69,17 @@ namespace MWSound /// minRange, and maxRange) Sound_Buffer* lookup(const ESM::RefId& soundId) const; + /// Lookup a sound by file name for its sound data (resource name, local volume, + /// minRange, and maxRange) + Sound_Buffer* lookup(std::string_view fileName) const; + /// Lookup a soundId for its sound data (resource name, local volume, /// minRange, and maxRange), and ensure it's ready for use. Sound_Buffer* load(const ESM::RefId& soundId); + // Lookup for a sound by file name, and ensure it's ready for use. + Sound_Buffer* load(std::string_view fileName); + void use(Sound_Buffer& sfx) { if (sfx.mUses++ == 0) @@ -92,6 +99,8 @@ namespace MWSound void clear(); private: + Sound_Buffer* loadSfx(Sound_Buffer* sfx); + Sound_Output* mOutput; std::deque mSoundBuffers; std::unordered_map mBufferNameMap; @@ -102,6 +111,7 @@ namespace MWSound std::deque mUnusedBuffers; inline Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); + inline Sound_Buffer* insertSound(std::string_view fileName); inline void unloadUnused(); }; diff --git a/apps/openmw/mwsound/soundmanagerimp.cpp b/apps/openmw/mwsound/soundmanagerimp.cpp index 146d947abd..cc2fecb07e 100644 --- a/apps/openmw/mwsound/soundmanagerimp.cpp +++ b/apps/openmw/mwsound/soundmanagerimp.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" @@ -36,6 +37,7 @@ namespace MWSound constexpr float sMinUpdateInterval = 1.0f / 30.0f; constexpr float sSfxFadeInDuration = 1.0f; constexpr float sSfxFadeOutDuration = 1.0f; + constexpr float sSoundCullDistance = 2000.f; WaterSoundUpdaterSettings makeWaterSoundUpdaterSettings() { @@ -357,7 +359,7 @@ namespace MWSound if (!mOutput->isInitialized()) return; - DecoderPtr decoder = loadVoice("Sound/" + filename); + DecoderPtr decoder = loadVoice(filename); if (!decoder) return; @@ -389,7 +391,7 @@ namespace MWSound if (!mOutput->isInitialized()) return; - DecoderPtr decoder = loadVoice("Sound/" + filename); + DecoderPtr decoder = loadVoice(filename); if (!decoder) return; @@ -486,14 +488,22 @@ namespace MWSound return mOutput->getStreamDelay(stream); } - Sound* SoundManager::playSound( - const ESM::RefId& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) + bool SoundManager::remove3DSoundAtDistance(PlayMode mode, const MWWorld::ConstPtr& ptr) const { if (!mOutput->isInitialized()) - return nullptr; + return true; - Sound_Buffer* sfx = mSoundBuffers.load(soundId); - if (!sfx) + const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); + const float squaredDist = (mListenerPos - objpos).length2(); + if ((mode & PlayMode::RemoveAtDistance) && squaredDist > sSoundCullDistance * sSoundCullDistance) + return true; + + return false; + } + + Sound* SoundManager::playSound(Sound_Buffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset) + { + if (!mOutput->isInitialized()) return nullptr; // Only one copy of given sound can be played at time, so stop previous copy @@ -517,25 +527,45 @@ namespace MWSound return result; } - Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId, float volume, float pitch, + Sound* SoundManager::playSound( + std::string_view fileName, float volume, float pitch, Type type, PlayMode mode, float offset) + { + if (!mOutput->isInitialized()) + return nullptr; + + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + Sound_Buffer* sfx = mSoundBuffers.load(normalizedName); + if (!sfx) + return nullptr; + + return playSound(sfx, volume, pitch, type, mode, offset); + } + + Sound* SoundManager::playSound( + const ESM::RefId& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) + { + if (!mOutput->isInitialized()) + return nullptr; + + Sound_Buffer* sfx = mSoundBuffers.load(soundId); + if (!sfx) + return nullptr; + + return playSound(sfx, volume, pitch, type, mode, offset); + } + + Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, Sound_Buffer* sfx, float volume, float pitch, Type type, PlayMode mode, float offset) { if (!mOutput->isInitialized()) return nullptr; - const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); - const float squaredDist = (mListenerPos - objpos).length2(); - if ((mode & PlayMode::RemoveAtDistance) && squaredDist > 2000 * 2000) - return nullptr; - - // Look up the sound in the ESM data - Sound_Buffer* sfx = mSoundBuffers.load(soundId); - if (!sfx) - return nullptr; - // Only one copy of given sound can be played at time on ptr, so stop previous copy stopSound(sfx, ptr); + const osg::Vec3f objpos(ptr.getRefData().getPosition().asVec3()); + const float squaredDist = (mListenerPos - objpos).length2(); + bool played; SoundPtr sound = getSoundRef(); if (!(mode & PlayMode::NoPlayerLocal) && ptr == MWMechanics::getPlayer()) @@ -578,6 +608,35 @@ namespace MWSound return result; } + Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId, float volume, float pitch, + Type type, PlayMode mode, float offset) + { + if (remove3DSoundAtDistance(mode, ptr)) + return nullptr; + + // Look up the sound in the ESM data + Sound_Buffer* sfx = mSoundBuffers.load(soundId); + if (!sfx) + return nullptr; + + return playSound3D(ptr, sfx, volume, pitch, type, mode, offset); + } + + Sound* SoundManager::playSound3D(const MWWorld::ConstPtr& ptr, std::string_view fileName, float volume, float pitch, + Type type, PlayMode mode, float offset) + { + if (remove3DSoundAtDistance(mode, ptr)) + return nullptr; + + // Look up the sound + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + Sound_Buffer* sfx = mSoundBuffers.load(normalizedName); + if (!sfx) + return nullptr; + + return playSound3D(ptr, sfx, volume, pitch, type, mode, offset); + } + Sound* SoundManager::playSound3D(const osg::Vec3f& initialPos, const ESM::RefId& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { @@ -644,6 +703,13 @@ namespace MWSound stopSound(sfx, ptr); } + void SoundManager::stopSound3D(const MWWorld::ConstPtr& ptr, std::string_view fileName) + { + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + auto soundId = ESM::RefId::stringRefId(normalizedName); + stopSound3D(ptr, soundId); + } + void SoundManager::stopSound3D(const MWWorld::ConstPtr& ptr) { SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); @@ -700,6 +766,13 @@ namespace MWSound } } + bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr& ptr, std::string_view fileName) const + { + std::string normalizedName = VFS::Path::normalizeFilename(fileName); + auto soundId = ESM::RefId::stringRefId(normalizedName); + return getSoundPlaying(ptr, soundId); + } + bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId) const { SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); @@ -849,8 +922,8 @@ namespace MWSound void SoundManager::cull3DSound(SoundBase* sound) { - // Hard-coded distance of 2000.0f is from vanilla Morrowind - const float maxDist = sound->getDistanceCull() ? 2000.0f : sound->getMaxDistance(); + // Hard-coded distance is from an original engine + const float maxDist = sound->getDistanceCull() ? sSoundCullDistance : sound->getMaxDistance(); const float squaredMaxDist = maxDist * maxDist; const osg::Vec3f pos = sound->getPosition(); diff --git a/apps/openmw/mwsound/soundmanagerimp.hpp b/apps/openmw/mwsound/soundmanagerimp.hpp index 474c8f50b1..7453ce86f4 100644 --- a/apps/openmw/mwsound/soundmanagerimp.hpp +++ b/apps/openmw/mwsound/soundmanagerimp.hpp @@ -132,6 +132,13 @@ namespace MWSound void cull3DSound(SoundBase* sound); + bool remove3DSoundAtDistance(PlayMode mode, const MWWorld::ConstPtr& ptr) const; + + Sound* playSound(Sound_Buffer* sfx, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0); + Sound* playSound3D(const MWWorld::ConstPtr& ptr, Sound_Buffer* sfx, float volume, float pitch, Type type, + PlayMode mode, float offset); + void updateSounds(float duration); void updateRegionSound(float duration); void updateWaterSound(); @@ -183,11 +190,11 @@ namespace MWSound void say(const MWWorld::ConstPtr& reference, const std::string& filename) override; ///< Make an actor say some text. - /// \param filename name of a sound file in "Sound/" in the data directory. + /// \param filename name of a sound file in the VFS void say(const std::string& filename) override; ///< Say some text, without an actor ref - /// \param filename name of a sound file in "Sound/" in the data directory. + /// \param filename name of a sound file in the VFS bool sayActive(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const override; ///< Is actor not speaking? @@ -219,12 +226,23 @@ namespace MWSound ///< Play a sound, independently of 3D-position ///< @param offset Number of seconds into the sound to start playback. + Sound* playSound(std::string_view fileName, float volume, float pitch, Type type = Type::Sfx, + PlayMode mode = PlayMode::Normal, float offset = 0) override; + ///< Play a sound, independently of 3D-position + ///< @param offset Number of seconds into the sound to start playback. + Sound* playSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId, float volume, float pitch, Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) override; ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless ///< Play_NoTrack is specified. ///< @param offset Number of seconds into the sound to start playback. + Sound* playSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName, float volume, float pitch, + Type type = Type::Sfx, PlayMode mode = PlayMode::Normal, float offset = 0) override; + ///< Play a 3D sound attached to an MWWorld::Ptr. Will be updated automatically with the Ptr's position, unless + ///< Play_NoTrack is specified. + ///< @param offset Number of seconds into the sound to start playback. + Sound* playSound3D(const osg::Vec3f& initialPos, const ESM::RefId& soundId, float volume, float pitch, Type type, PlayMode mode, float offset = 0) override; ///< Play a 3D sound at \a initialPos. If the sound should be moving, it must be updated using @@ -236,7 +254,10 @@ namespace MWSound /// @note no-op if \a sound is null void stopSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) override; - ///< Stop the given object from playing the given sound, + ///< Stop the given object from playing the given sound. + + void stopSound3D(const MWWorld::ConstPtr& reference, std::string_view fileName) override; + ///< Stop the given object from playing the given sound. void stopSound3D(const MWWorld::ConstPtr& reference) override; ///< Stop the given object from playing all sounds. @@ -253,6 +274,9 @@ namespace MWSound bool getSoundPlaying(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) const override; ///< Is the given sound currently playing on the given object? + bool getSoundPlaying(const MWWorld::ConstPtr& reference, std::string_view fileName) const override; + ///< Is the given sound currently playing on the given object? + void pauseSounds(MWSound::BlockerType blocker, int types = int(Type::Mask)) override; ///< Pauses all currently playing sounds, including music. diff --git a/components/misc/resourcehelpers.cpp b/components/misc/resourcehelpers.cpp index 19fa9d8775..55fef5cdbd 100644 --- a/components/misc/resourcehelpers.cpp +++ b/components/misc/resourcehelpers.cpp @@ -151,6 +151,11 @@ std::string Misc::ResourceHelpers::correctMeshPath(const std::string& resPath, c return "meshes\\" + resPath; } +std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath) +{ + return "sound\\" + resPath; +} + std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath) { constexpr std::string_view prefix = "meshes"; diff --git a/components/misc/resourcehelpers.hpp b/components/misc/resourcehelpers.hpp index 2ab92e0bed..0597c7bc16 100644 --- a/components/misc/resourcehelpers.hpp +++ b/components/misc/resourcehelpers.hpp @@ -35,6 +35,9 @@ namespace Misc // Adds "meshes\\". std::string correctMeshPath(const std::string& resPath, const VFS::Manager* vfs); + // Adds "sound\\". + std::string correctSoundPath(const std::string& resPath); + // Removes "meshes\\". std::string_view meshPathForESM3(std::string_view resPath); diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 4165eb3119..e60f22b7fa 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -19,6 +19,7 @@ Lua API reference openmw_self openmw_nearby openmw_input + openmw_ambient openmw_ui openmw_camera openmw_postprocessing diff --git a/docs/source/reference/lua-scripting/openmw_ambient.rst b/docs/source/reference/lua-scripting/openmw_ambient.rst new file mode 100644 index 0000000000..a68f5f4469 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_ambient.rst @@ -0,0 +1,5 @@ +Package openmw.ambient +====================== + +.. raw:: html + :file: generated_html/openmw_ambient.html diff --git a/docs/source/reference/lua-scripting/tables/packages.rst b/docs/source/reference/lua-scripting/tables/packages.rst index 43f263f8f0..e746274e6d 100644 --- a/docs/source/reference/lua-scripting/tables/packages.rst +++ b/docs/source/reference/lua-scripting/tables/packages.rst @@ -21,6 +21,8 @@ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.nearby ` | by local scripts | | Read-only access to the nearest area of the game world. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw.ambient ` | by player scripts | | Controls background sounds for given player. | ++------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.input ` | by player scripts | | User input. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.ui ` | by player scripts | | Controls :ref:`user interface `. | diff --git a/files/lua_api/CMakeLists.txt b/files/lua_api/CMakeLists.txt index a0d7570884..87f5bfe91b 100644 --- a/files/lua_api/CMakeLists.txt +++ b/files/lua_api/CMakeLists.txt @@ -9,6 +9,7 @@ set(LUA_API_FILES math.doclua string.doclua table.doclua + openmw/ambient.lua openmw/async.lua openmw/core.lua openmw/nearby.lua diff --git a/files/lua_api/openmw/ambient.lua b/files/lua_api/openmw/ambient.lua new file mode 100644 index 0000000000..3722cb8c0f --- /dev/null +++ b/files/lua_api/openmw/ambient.lua @@ -0,0 +1,75 @@ +--- +-- `openmw.ambient` controls background sounds, specific to given player (2D-sounds). +-- Can be used only by local scripts, that are attached to a player. +-- @module ambient +-- @usage local ambient = require('openmw.ambient') + + + +--- +-- Play a 2D sound +-- @function [parent=#ambient] playSound +-- @param #string soundId ID of Sound record to play +-- @param #table options An optional table with additional optional arguments. Can contain: +-- +-- * `timeOffset` - a floating point number >= 0, to some time (in second) from beginning of sound file (default: 0); +-- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); +-- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); +-- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); +-- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); +-- @usage local params = { +-- timeOffset=0.1 +-- volume=0.3, +-- scale=false, +-- pitch=1.0, +-- loop=true +-- }; +-- ambient.playSound("shock bolt", params) + +--- +-- Play a 2D sound file +-- @function [parent=#ambient] playSoundFile +-- @param #string fileName Path to sound file in VFS +-- @param #table options An optional table with additional optional arguments. Can contain: +-- +-- * `timeOffset` - a floating point number >= 0, to some time (in second) from beginning of sound file (default: 0); +-- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); +-- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); +-- * `scale` - a boolean, to set if sound pitch should be scaled by simulation time scaling (default: true); +-- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); +-- @usage local params = { +-- timeOffset=0.1 +-- volume=0.3, +-- scale=false, +-- pitch=1.0, +-- loop=true +-- }; +-- ambient.playSoundFile("Sound\\test.mp3", params) + +--- +-- Stop a sound +-- @function [parent=#ambient] stopSound +-- @param #string soundId ID of Sound record to stop +-- @usage ambient.stopSound("shock bolt"); + +--- +-- Stop a sound file +-- @function [parent=#ambient] stopSoundFile +-- @param #string fileName Path to sound file in VFS +-- @usage ambient.stopSoundFile("Sound\\test.mp3"); + +--- +-- Check if sound is playing +-- @function [parent=#ambient] isSoundPlaying +-- @param #string soundId ID of Sound record to check +-- @return #boolean +-- @usage local isPlaying = ambient.isSoundPlaying("shock bolt"); + +--- +-- Check if sound file is playing +-- @function [parent=#ambient] isSoundFilePlaying +-- @param #string fileName Path to sound file in VFS +-- @return #boolean +-- @usage local isPlaying = ambient.isSoundFilePlaying("Sound\\test.mp3"); + +return nil diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 40389aefa8..982f342642 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -739,4 +739,127 @@ -- @field #number magnitudeBase -- @field #number magnitudeModifier +--- +-- Play a 3D sound, attached to object +-- @function [parent=#core] playSound3d +-- @param #string soundId ID of Sound record to play +-- @param #GameObject object Object to which we attach the sound +-- @param #table options An optional table with additional optional arguments. Can contain: +-- +-- * `timeOffset` - a floating point number >= 0, to some time (in second) from beginning of sound file (default: 0); +-- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); +-- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); +-- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); +-- @usage local params = { +-- timeOffset=0.1 +-- volume=0.3, +-- loop=false, +-- pitch=1.0 +-- }; +-- core.sound.playSound3d("shock bolt", object, params) + +--- +-- Play a 3D sound file, attached to object +-- @function [parent=#core] playSoundFile3d +-- @param #string fileName Path to sound file in VFS +-- @param #GameObject object Object to which we attach the sound +-- @param #table options An optional table with additional optional arguments. Can contain: +-- +-- * `timeOffset` - a floating point number >= 0, to some time (in second) from beginning of sound file (default: 0); +-- * `volume` - a floating point number >= 0, to set a sound volume (default: 1); +-- * `pitch` - a floating point number >= 0, to set a sound pitch (default: 1); +-- * `loop` - a boolean, to set if sound should be repeated when it ends (default: false); +-- @usage local params = { +-- timeOffset=0.1 +-- volume=0.3, +-- loop=false, +-- pitch=1.0 +-- }; +-- core.sound.playSoundFile3d("Sound\\test.mp3", object, params) + +--- +-- Stop a 3D sound, attached to object +-- @function [parent=#core] stopSound3d +-- @param #string soundId ID of Sound record to stop +-- @param #GameObject object Object on which we want to stop sound +-- @usage core.sound.stopSound("shock bolt", object); + +--- +-- Stop a 3D sound file, attached to object +-- @function [parent=#core] stopSoundFile3d +-- @param #string fileName Path to sound file in VFS +-- @param #GameObject object Object on which we want to stop sound +-- @usage core.sound.stopSoundFile("Sound\\test.mp3", object); + +--- +-- Check if sound is playing on given object +-- @function [parent=#core] isSoundPlaying +-- @param #string soundId ID of Sound record to check +-- @param #GameObject object Object on which we want to check sound +-- @return #boolean +-- @usage local isPlaying = core.sound.isSoundPlaying("shock bolt", object); + +--- +-- Check if sound file is playing on given object +-- @function [parent=#core] isSoundFilePlaying +-- @param #string fileName Path to sound file in VFS +-- @param #GameObject object Object on which we want to check sound +-- @return #boolean +-- @usage local isPlaying = core.sound.isSoundFilePlaying("Sound\\test.mp3", object); + +--- +-- Play an animated voiceover. Has two overloads: +-- +-- * With an "object" argument: play sound for given object, with speaking animation if possible equipment slots. +-- * Without an "object" argument: play sound globally, without object +-- @function [parent=#core] say +-- @param #string fileName Path to sound file in VFS +-- @param #GameObject object Object on which we want to play an animated voiceover (optional) +-- @param #string text Subtitle text (optional) +-- @usage -- play voiceover for object and print messagebox +-- core.sound.say("Sound\\Vo\\Misc\\voice.mp3", object, "Subtitle text") +-- @usage -- play voiceover globally and print messagebox +-- core.sound.say("Sound\\Vo\\Misc\\voice.mp3", "Subtitle text") +-- @usage -- play voiceover for object without messagebox +-- core.sound.say("Sound\\Vo\\Misc\\voice.mp3", object) +-- @usage -- play voiceover globally without messagebox +-- core.sound.say("Sound\\Vo\\Misc\\voice.mp3") + +--- +-- Stop animated voiceover +-- @function [parent=#core] stopSay +-- @param #string fileName Path to sound file in VFS +-- @param #GameObject object Object on which we want to stop an animated voiceover (optional) +-- @usage -- stop voice for given object +-- core.sound.stopSay(object); +-- @usage -- stop global voice +-- core.sound.stopSay(); + +--- +-- Check if animated voiceover is playing +-- @function [parent=#core] isSayActive +-- @param #GameObject object Object on which we want to check an animated voiceover (optional) +-- @return #boolean +-- @usage -- check voice for given object +-- local isActive = isSayActive(object); +-- @usage -- check global voice +-- local isActive = isSayActive(); + +--- +-- @type Sound +-- @field #string id Sound id +-- @field #string fileName Normalized path to sound file in VFS +-- @field #number volume Raw sound volume, from 0 to 255 +-- @field #number minRange Raw minimal range value, from 0 to 255 +-- @field #number maxRange Raw maximal range value, from 0 to 255 + +--- List of all @{#Sound}s. +-- @field [parent=#Sound] #list<#Sound> sounds +-- @usage local sound = core.sound.sounds['Ashstorm'] -- get by id +-- @usage local sound = core.sound.sounds[1] -- get by index +-- @usage -- Print all sound files paths +-- for _, sound in pairs(core.sound.sounds) do +-- print(sound.fileName) +-- end + return nil diff --git a/files/lua_api/openmw/types.lua b/files/lua_api/openmw/types.lua index c9ca478529..cd28f9bd12 100644 --- a/files/lua_api/openmw/types.lua +++ b/files/lua_api/openmw/types.lua @@ -127,10 +127,10 @@ --- -- Get equipment. -- Has two overloads: --- 1) With a single argument: returns a table `slot` -> @{openmw.core#GameObject} of currently equipped items. --- See @{#EQUIPMENT_SLOT}. Returns empty table if the actor doesn't have --- equipment slots. --- 2) With two arguments: returns an item equipped to the given slot. +-- +-- * With a single argument: returns a table `slot` -> @{openmw.core#GameObject} of currently equipped items. +-- See @{#EQUIPMENT_SLOT}. Returns empty table if the actor doesn't have equipment slots. +-- * With two arguments: returns an item equipped to the given slot. -- @function [parent=#Actor] getEquipment -- @param openmw.core#GameObject actor -- @param #number slot Optional number of the equipment slot