Merge branch 'esm4sound' into 'master'
Some checks failed
Build and test / Ubuntu (push) Has been cancelled
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled

Initial support of ESM4 sounds

See merge request OpenMW/openmw!3784
This commit is contained in:
Petr Mikheev 2025-04-24 12:21:03 +00:00
commit 04d492be4f
11 changed files with 96 additions and 28 deletions

View file

@ -26,6 +26,15 @@ namespace
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({}); std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({});
constexpr VFS::Path::NormalizedView path("sound/foo.wav"); constexpr VFS::Path::NormalizedView path("sound/foo.wav");
EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3"); EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3");
auto correctESM4SoundPath = [](auto path, auto* vfs) {
return Misc::ResourceHelpers::correctResourcePath({ { "sound" } }, path, vfs, ".mp3");
};
EXPECT_EQ(correctESM4SoundPath("foo.WAV", mVFS.get()), "sound\\foo.mp3");
EXPECT_EQ(correctESM4SoundPath("SOUND/foo.WAV", mVFS.get()), "sound\\foo.mp3");
EXPECT_EQ(correctESM4SoundPath("DATA\\SOUND\\foo.WAV", mVFS.get()), "sound\\foo.mp3");
EXPECT_EQ(correctESM4SoundPath("\\Data/Sound\\foo.WAV", mVFS.get()), "sound\\foo.mp3");
} }
namespace namespace

View file

@ -149,5 +149,9 @@ namespace MWLua
addModelProperty(record); addModelProperty(record);
record["isAutomatic"] = sol::readonly_property( record["isAutomatic"] = sol::readonly_property(
[](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; });
record["openSound"] = sol::readonly_property(
[](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mOpenSound).serializeText(); });
record["closeSound"] = sol::readonly_property(
[](const ESM4::Door& rec) -> std::string { return ESM::RefId(rec.mCloseSound).serializeText(); });
} }
} }

View file

@ -5,7 +5,10 @@
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/esm3/loadsoun.hpp> #include <components/esm3/loadsoun.hpp>
#include <components/esm4/loadsndr.hpp>
#include <components/esm4/loadsoun.hpp>
#include <components/misc/resourcehelpers.hpp> #include <components/misc/resourcehelpers.hpp>
#include <components/resource/resourcesystem.hpp>
#include <components/settings/values.hpp> #include <components/settings/values.hpp>
#include <components/vfs/pathutil.hpp> #include <components/vfs/pathutil.hpp>
@ -99,7 +102,12 @@ namespace MWSound
{ {
if (mBufferNameMap.empty()) if (mBufferNameMap.empty())
{ {
for (const ESM::Sound& sound : MWBase::Environment::get().getESMStore()->get<ESM::Sound>()) const MWWorld::ESMStore* esmstore = MWBase::Environment::get().getESMStore();
for (const ESM::Sound& sound : esmstore->get<ESM::Sound>())
insertSound(sound.mId, sound);
for (const ESM4::Sound& sound : esmstore->get<ESM4::Sound>())
insertSound(sound.mId, sound);
for (const ESM4::SoundReference& sound : esmstore->get<ESM4::SoundReference>())
insertSound(sound.mId, sound); insertSound(sound.mId, sound);
} }
@ -190,6 +198,28 @@ namespace MWSound
return &sfx; return &sfx;
} }
Sound_Buffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound)
{
std::string path = Misc::ResourceHelpers::correctResourcePath(
{ { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3");
float volume = 1, min = 1, max = 255; // TODO: needs research
Sound_Buffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max);
mBufferNameMap.emplace(soundId, &sfx);
return &sfx;
}
Sound_Buffer* SoundBufferPool::insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound)
{
std::string path = Misc::ResourceHelpers::correctResourcePath(
{ { "sound" } }, sound.mSoundFile, MWBase::Environment::get().getResourceSystem()->getVFS(), ".mp3");
float volume = 1, min = 1, max = 255; // TODO: needs research
// TODO: sound.mSoundId can link to another SoundReference, probably we will need to add additional lookups to
// ESMStore.
Sound_Buffer& sfx = mSoundBuffers.emplace_back(VFS::Path::Normalized(std::move(path)), volume, min, max);
mBufferNameMap.emplace(soundId, &sfx);
return &sfx;
}
void SoundBufferPool::unloadUnused() void SoundBufferPool::unloadUnused()
{ {
while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin) while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin)

View file

@ -14,6 +14,12 @@ namespace ESM
struct Sound; struct Sound;
} }
namespace ESM4
{
struct Sound;
struct SoundReference;
}
namespace VFS namespace VFS
{ {
class Manager; class Manager;
@ -111,10 +117,12 @@ namespace MWSound
// NOTE: unused buffers are stored in front-newest order. // NOTE: unused buffers are stored in front-newest order.
std::deque<Sound_Buffer*> mUnusedBuffers; std::deque<Sound_Buffer*> mUnusedBuffers;
inline Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound); Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM::Sound& sound);
inline Sound_Buffer* insertSound(std::string_view fileName); Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM4::Sound& sound);
Sound_Buffer* insertSound(const ESM::RefId& soundId, const ESM4::SoundReference& sound);
Sound_Buffer* insertSound(std::string_view fileName);
inline void unloadUnused(); void unloadUnused();
}; };
} }

View file

@ -105,6 +105,8 @@ namespace ESM4
struct Potion; struct Potion;
struct Race; struct Race;
struct Reference; struct Reference;
struct Sound;
struct SoundReference;
struct Static; struct Static;
struct StaticCollection; struct StaticCollection;
struct Terminal; struct Terminal;
@ -146,8 +148,8 @@ namespace MWWorld
Store<ESM4::Land>, Store<ESM4::LandTexture>, Store<ESM4::LevelledCreature>, Store<ESM4::LevelledItem>, Store<ESM4::Land>, Store<ESM4::LandTexture>, Store<ESM4::LevelledCreature>, Store<ESM4::LevelledItem>,
Store<ESM4::LevelledNpc>, Store<ESM4::Light>, Store<ESM4::MiscItem>, Store<ESM4::MovableStatic>, Store<ESM4::LevelledNpc>, Store<ESM4::Light>, Store<ESM4::MiscItem>, Store<ESM4::MovableStatic>,
Store<ESM4::Npc>, Store<ESM4::Outfit>, Store<ESM4::Potion>, Store<ESM4::Race>, Store<ESM4::Reference>, Store<ESM4::Npc>, Store<ESM4::Outfit>, Store<ESM4::Potion>, Store<ESM4::Race>, Store<ESM4::Reference>,
Store<ESM4::Static>, Store<ESM4::StaticCollection>, Store<ESM4::Terminal>, Store<ESM4::Tree>, Store<ESM4::Sound>, Store<ESM4::SoundReference>, Store<ESM4::Static>, Store<ESM4::StaticCollection>,
Store<ESM4::Weapon>, Store<ESM4::World>>; Store<ESM4::Terminal>, Store<ESM4::Tree>, Store<ESM4::Weapon>, Store<ESM4::World>>;
private: private:
template <typename T> template <typename T>

View file

@ -1349,6 +1349,8 @@ template class MWWorld::TypedDynamicStore<ESM4::Npc>;
template class MWWorld::TypedDynamicStore<ESM4::Outfit>; template class MWWorld::TypedDynamicStore<ESM4::Outfit>;
template class MWWorld::TypedDynamicStore<ESM4::Potion>; template class MWWorld::TypedDynamicStore<ESM4::Potion>;
template class MWWorld::TypedDynamicStore<ESM4::Race>; template class MWWorld::TypedDynamicStore<ESM4::Race>;
template class MWWorld::TypedDynamicStore<ESM4::Sound>;
template class MWWorld::TypedDynamicStore<ESM4::SoundReference>;
template class MWWorld::TypedDynamicStore<ESM4::Static>; template class MWWorld::TypedDynamicStore<ESM4::Static>;
template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>; template class MWWorld::TypedDynamicStore<ESM4::StaticCollection>;
template class MWWorld::TypedDynamicStore<ESM4::Terminal>; template class MWWorld::TypedDynamicStore<ESM4::Terminal>;

View file

@ -74,6 +74,8 @@
#include <components/esm4/loadrace.hpp> #include <components/esm4/loadrace.hpp>
#include <components/esm4/loadrefr.hpp> #include <components/esm4/loadrefr.hpp>
#include <components/esm4/loadscol.hpp> #include <components/esm4/loadscol.hpp>
#include <components/esm4/loadsndr.hpp>
#include <components/esm4/loadsoun.hpp>
#include <components/esm4/loadstat.hpp> #include <components/esm4/loadstat.hpp>
#include <components/esm4/loadterm.hpp> #include <components/esm4/loadterm.hpp>
#include <components/esm4/loadtree.hpp> #include <components/esm4/loadtree.hpp>

View file

@ -33,14 +33,10 @@ bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path)
return changeExtension(path, ".dds"); return changeExtension(path, ".dds");
} }
std::string Misc::ResourceHelpers::correctResourcePath( // If `ext` is not empty we first search file with extension `ext`, then if not found fallback to original extension.
std::span<const std::string_view> topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs) std::string Misc::ResourceHelpers::correctResourcePath(std::span<const std::string_view> topLevelDirectories,
std::string_view resPath, const VFS::Manager* vfs, std::string_view ext)
{ {
/* Bethesda at some point converted all their BSA
* textures from tga to dds for increased load speed, but all
* texture file name references were kept as .tga.
*/
std::string correctedPath = Misc::StringUtils::lowerCase(resPath); std::string correctedPath = Misc::StringUtils::lowerCase(resPath);
// Flatten slashes // Flatten slashes
@ -80,14 +76,14 @@ std::string Misc::ResourceHelpers::correctResourcePath(
std::string origExt = correctedPath; std::string origExt = correctedPath;
// since we know all (GOTY edition or less) textures end // replace extension if `ext` is specified (used for .tga -> .dds, .wav -> .mp3)
// in .dds, we change the extension bool isExtChanged = !ext.empty() && changeExtension(correctedPath, ext);
bool changedToDds = changeExtensionToDds(correctedPath);
if (vfs->exists(correctedPath)) if (vfs->exists(correctedPath))
return correctedPath; return correctedPath;
// if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods)
// verify, and revert if false (this call succeeds quickly, but fails slowly) // fall back to original extension
if (changedToDds && vfs->exists(origExt)) if (isExtChanged && vfs->exists(origExt))
return origExt; return origExt;
// fall back to a resource in the top level directory if it exists // fall back to a resource in the top level directory if it exists
@ -98,7 +94,7 @@ std::string Misc::ResourceHelpers::correctResourcePath(
if (vfs->exists(fallback)) if (vfs->exists(fallback))
return fallback; return fallback;
if (changedToDds) if (isExtChanged)
{ {
fallback = topLevelDirectories.front(); fallback = topLevelDirectories.front();
fallback += '\\'; fallback += '\\';
@ -110,19 +106,23 @@ std::string Misc::ResourceHelpers::correctResourcePath(
return correctedPath; return correctedPath;
} }
// Note: Bethesda at some point converted all their BSA textures from tga to dds for increased load speed,
// but all texture file name references were kept as .tga. So we pass ext=".dds" to all helpers
// looking for textures.
std::string Misc::ResourceHelpers::correctTexturePath(std::string_view resPath, const VFS::Manager* vfs) std::string Misc::ResourceHelpers::correctTexturePath(std::string_view resPath, const VFS::Manager* vfs)
{ {
return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs); return correctResourcePath({ { "textures", "bookart" } }, resPath, vfs, ".dds");
} }
std::string Misc::ResourceHelpers::correctIconPath(std::string_view resPath, const VFS::Manager* vfs) std::string Misc::ResourceHelpers::correctIconPath(std::string_view resPath, const VFS::Manager* vfs)
{ {
return correctResourcePath({ { "icons" } }, resPath, vfs); return correctResourcePath({ { "icons" } }, resPath, vfs, ".dds");
} }
std::string Misc::ResourceHelpers::correctBookartPath(std::string_view resPath, const VFS::Manager* vfs) std::string Misc::ResourceHelpers::correctBookartPath(std::string_view resPath, const VFS::Manager* vfs)
{ {
return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs); return correctResourcePath({ { "bookart", "textures" } }, resPath, vfs, ".dds");
} }
std::string Misc::ResourceHelpers::correctBookartPath( std::string Misc::ResourceHelpers::correctBookartPath(
@ -199,6 +199,12 @@ std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath
VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath( VFS::Path::Normalized Misc::ResourceHelpers::correctSoundPath(
VFS::Path::NormalizedView resPath, const VFS::Manager& vfs) VFS::Path::NormalizedView resPath, const VFS::Manager& vfs)
{ {
// Note: likely should be replaced with
// return correctResourcePath({ { "sound" } }, resPath, vfs, ".mp3");
// but there is a slight difference in behaviour:
// - `correctResourcePath(..., ".mp3")` first checks `.mp3`, then tries the original extension
// - the implementation below first tries the original extension, then falls back to `.mp3`.
// Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav. // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
if (!vfs.exists(resPath)) if (!vfs.exists(resPath))
{ {

View file

@ -1,12 +1,12 @@
#ifndef MISC_RESOURCEHELPERS_H #ifndef MISC_RESOURCEHELPERS_H
#define MISC_RESOURCEHELPERS_H #define MISC_RESOURCEHELPERS_H
#include <components/vfs/pathutil.hpp>
#include <span> #include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <components/vfs/pathutil.hpp>
namespace VFS namespace VFS
{ {
class Manager; class Manager;
@ -25,8 +25,8 @@ namespace Misc
namespace ResourceHelpers namespace ResourceHelpers
{ {
bool changeExtensionToDds(std::string& path); bool changeExtensionToDds(std::string& path);
std::string correctResourcePath( std::string correctResourcePath(std::span<const std::string_view> topLevelDirectories, std::string_view resPath,
std::span<const std::string_view> topLevelDirectories, std::string_view resPath, const VFS::Manager* vfs); const VFS::Manager* vfs, std::string_view ext = "");
std::string correctTexturePath(std::string_view resPath, const VFS::Manager* vfs); std::string correctTexturePath(std::string_view resPath, const VFS::Manager* vfs);
std::string correctIconPath(std::string_view resPath, const VFS::Manager* vfs); std::string correctIconPath(std::string_view resPath, const VFS::Manager* vfs);
std::string correctBookartPath(std::string_view resPath, const VFS::Manager* vfs); std::string correctBookartPath(std::string_view resPath, const VFS::Manager* vfs);
@ -49,6 +49,7 @@ namespace Misc
std::string_view meshPathForESM3(std::string_view resPath); std::string_view meshPathForESM3(std::string_view resPath);
VFS::Path::Normalized correctSoundPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs); VFS::Path::Normalized correctSoundPath(VFS::Path::NormalizedView resPath, const VFS::Manager& vfs);
std::string correctESM4SoundPath(std::string_view resPath, const VFS::Manager* vfs);
/// marker objects that have a hardcoded function in the game logic, should be hidden from the player /// marker objects that have a hardcoded function in the game logic, should be hidden from the player
bool isHiddenMarker(const ESM::RefId& id); bool isHiddenMarker(const ESM::RefId& id);

View file

@ -1,4 +1,5 @@
local async = require('openmw.async') local async = require('openmw.async')
local core = require('openmw.core')
local types = require('openmw.types') local types = require('openmw.types')
local world = require('openmw.world') local world = require('openmw.world')
@ -6,8 +7,9 @@ local EnableObject = async:registerTimerCallback('EnableObject', function(obj) o
local function ESM4DoorActivation(door, actor) local function ESM4DoorActivation(door, actor)
-- TODO: Implement lockpicking minigame -- TODO: Implement lockpicking minigame
-- TODO: Play door opening animation and sound -- TODO: Play door opening animation
local Door4 = types.ESM4Door local Door4 = types.ESM4Door
core.sound.playSound3d(Door4.record(door).openSound, actor)
if Door4.isTeleport(door) then if Door4.isTeleport(door) then
actor:teleport(Door4.destCell(door), Door4.destPosition(door), Door4.destRotation(door)) actor:teleport(Door4.destCell(door), Door4.destPosition(door), Door4.destRotation(door))
else else

View file

@ -2449,5 +2449,7 @@
-- @field #string id Record id -- @field #string id Record id
-- @field #string name Human-readable name -- @field #string name Human-readable name
-- @field #string model VFS path to the model -- @field #string model VFS path to the model
-- @field #string openSound FormId of the door opening sound
-- @field #string closeSound FormId of the door closing sound
return nil return nil