Merge branch 'sounds' into 'master'

Lua bindings for sound functions

See merge request OpenMW/openmw!3247
macos_ci_fix
Petr Mikheev 9 months ago
commit 4211665ede

@ -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

@ -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
)

@ -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.

@ -23,6 +23,8 @@
#include <components/interpreter/defines.hpp>
#include <components/interpreter/interpreter.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/settings/values.hpp>
#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);
}

@ -4,6 +4,7 @@
#include <components/debug/debuglog.hpp>
#include <components/fallback/fallback.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/rng.hpp>
#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()

@ -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<std::string>& contentList = MWBase::Environment::get().getWorld()->getContentFiles();
for (size_t i = 0; i < contentList.size(); ++i)
@ -331,6 +333,7 @@ namespace MWLua
std::map<std::string, sol::object> initPlayerPackages(const Context& context)
{
return {
{ "openmw.ambient", initAmbientPackage(context) },
{ "openmw.camera", initCameraPackage(context.mLua->sol()) },
{ "openmw.debug", initDebugPackage(context) },
{ "openmw.input", initInputPackage(context) },

@ -0,0 +1,198 @@
#include "soundbindings.hpp"
#include "luabindings.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include <components/esm3/loadsoun.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/vfs/pathutil.hpp>
#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<sol::table>& 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<sol::table>& 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<sol::table>& 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<sol::table>& 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<sol::table>& 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<std::string_view> 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<std::string_view> 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<ESM::Sound>;
const SoundStore* soundStore = &MWBase::Environment::get().getWorld()->getStore().get<ESM::Sound>();
sol::usertype<SoundStore> soundStoreT = lua.new_usertype<SoundStore>("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<sol::function>();
soundStoreT[sol::meta_function::ipairs] = lua["ipairsForArray"].template get<sol::function>();
api["sounds"] = soundStore;
// Sound record
auto soundT = lua.new_usertype<ESM::Sound>("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);
}
}

@ -0,0 +1,15 @@
#ifndef MWLUA_SOUNDBINDINGS_H
#define MWLUA_SOUNDBINDINGS_H
#include <sol/forward.hpp>
#include "context.hpp"
namespace MWLua
{
sol::table initCoreSoundBindings(const Context&);
sol::table initAmbientPackage(const Context& context);
}
#endif // MWLUA_SOUNDBINDINGS_H

@ -5,6 +5,7 @@
#include <components/interpreter/interpreter.hpp>
#include <components/interpreter/opcodes.hpp>
#include <components/interpreter/runtime.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/settings/values.hpp>
#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);

@ -5,6 +5,7 @@
#include <components/debug/debuglog.hpp>
#include <components/esm3/loadsoun.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/settings/settings.hpp>
#include <components/vfs/pathutil.hpp>
@ -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)
{
auto [handle, size] = mOutput->loadSound(sfx->getResourceName());
if (handle == nullptr)
return {};
return loadSfx(sfx);
}
sfx->mHandle = handle;
Sound_Buffer* SoundBufferPool::load(std::string_view fileName)
{
auto soundId = ESM::RefId::stringRefId(fileName);
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);
Sound_Buffer* sfx;
const auto it = mBufferNameMap.find(soundId);
if (it != mBufferNameMap.end())
sfx = it->second;
else
{
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<ESM::GameSetting>());
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);

@ -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<Sound_Buffer> mSoundBuffers;
std::unordered_map<ESM::RefId, Sound_Buffer*> mBufferNameMap;
@ -102,6 +111,7 @@ namespace MWSound
std::deque<Sound_Buffer*> mUnusedBuffers;
inline Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound);
inline Sound_Buffer* insertSound(std::string_view fileName);
inline void unloadUnused();
};

@ -11,6 +11,7 @@
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/rng.hpp>
#include <components/vfs/manager.hpp>
#include <components/vfs/pathutil.hpp>
#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,
Type type, PlayMode mode, float offset)
Sound* SoundManager::playSound(
std::string_view fileName, 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)
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;
// Look up the sound in the ESM data
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;
// 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();

@ -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.

@ -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";

@ -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);

@ -19,6 +19,7 @@ Lua API reference
openmw_self
openmw_nearby
openmw_input
openmw_ambient
openmw_ui
openmw_camera
openmw_postprocessing

@ -0,0 +1,5 @@
Package openmw.ambient
======================
.. raw:: html
:file: generated_html/openmw_ambient.html

@ -21,6 +21,8 @@
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.nearby <Package openmw.nearby>` | by local scripts | | Read-only access to the nearest area of the game world. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.ambient <Package openmw.ambient>` | by player scripts | | Controls background sounds for given player. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.input <Package openmw.input>` | by player scripts | | User input. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.ui <Package openmw.ui>` | by player scripts | | Controls :ref:`user interface <User interface reference>`. |

@ -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

@ -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

@ -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

@ -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

Loading…
Cancel
Save