Merge branch 'music' into 'master'

Rework music system

See merge request OpenMW/openmw!3372
macos_ci_fix
psi29a 1 year ago
commit 12159d95f3

@ -75,6 +75,7 @@
Feature #3537: Shader-based water ripples
Feature #5492: Let rain and snow collide with statics
Feature #6149: Dehardcode Lua API_REVISION
Feature #6152: Playing music via lua scripts
Feature #6447: Add LOD support to Object Paging
Feature #6491: Add support for Qt6
Feature #6556: Lua API for sounds
@ -99,6 +100,7 @@
Feature #7477: NegativeLight Magic Effect flag
Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field
Feature #7546: Start the game on Fredas
Feature #7568: Uninterruptable scripted music
Task #5896: Do not use deprecated MyGUI properties
Task #7113: Move from std::atoi to std::from_char
Task #7117: Replace boost::scoped_array with std::vector

@ -71,7 +71,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 49)
set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 47)
set(OPENMW_LUA_API_REVISION 48)
set(OPENMW_VERSION_COMMITHASH "")
set(OPENMW_VERSION_TAGHASH "")

@ -913,7 +913,13 @@ void OMW::Engine::go()
{
// start in main menu
mWindowManager->pushGuiMode(MWGui::GM_MainMenu);
mSoundManager->playPlaylist("Title");
std::string titlefile = "music/special/morrowind title.mp3";
if (mVFS->exists(titlefile))
mSoundManager->streamMusic(titlefile, MWSound::MusicType::Special);
else
Log(Debug::Warning) << "Title music not found";
std::string_view logo = Fallback::Map::getString("Movies_Morrowind_Logo");
if (!logo.empty())
mWindowManager->playVideo(logo, /*allowSkipping*/ true, /*overrideSounds*/ false);

@ -26,6 +26,11 @@ namespace ESM
class ESMWriter;
}
namespace MWSound
{
enum class MusicType;
}
namespace MWWorld
{
class Ptr;
@ -282,6 +287,9 @@ namespace MWBase
virtual float getAngleToPlayer(const MWWorld::Ptr& ptr) const = 0;
virtual MWMechanics::GreetingState getGreetingState(const MWWorld::Ptr& ptr) const = 0;
virtual bool isTurningToPlayer(const MWWorld::Ptr& ptr) const = 0;
virtual MWSound::MusicType getMusicType() const = 0;
virtual void setMusicType(MWSound::MusicType type) = 0;
};
}

@ -29,6 +29,14 @@ namespace MWSound
MaxCount
};
enum class MusicType
{
Special,
Explore,
Battle,
Scripted
};
class Sound;
class Stream;
struct Sound_Decoder;
@ -101,12 +109,17 @@ namespace MWBase
virtual void processChangedSettings(const std::set<std::pair<std::string, std::string>>& settings) = 0;
virtual bool isEnabled() const = 0;
///< Returns true if sound system is enabled
virtual void stopMusic() = 0;
///< Stops music if it's playing
virtual void streamMusic(const std::string& filename) = 0;
virtual void streamMusic(const std::string& filename, MWSound::MusicType type, float fade = 1.f) = 0;
///< Play a soundifle
/// \param filename name of a sound file in "Music/" in the data directory.
/// \param filename name of a sound file in the data directory.
/// \param type music type.
/// \param fade time in seconds to fade out current track before start this one.
virtual bool isMusicPlaying() = 0;
///< Returns true if music is playing

@ -214,7 +214,8 @@ namespace MWGui
center();
// Play LevelUp Music
MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Triumph.mp3");
MWBase::Environment::get().getSoundManager()->streamMusic(
"Music/Special/MW_Triumph.mp3", MWSound::MusicType::Special);
}
void LevelupDialog::onOkButtonClicked(MyGUI::Widget* sender)

@ -95,6 +95,15 @@ namespace MWLua
return MWBase::Environment::get().getSoundManager()->getSoundPlaying(MWWorld::Ptr(), fileName);
};
api["streamMusic"] = [](std::string_view fileName) {
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
sndMgr->streamMusic(std::string(fileName), MWSound::MusicType::Scripted);
};
api["isMusicPlaying"] = []() { return MWBase::Environment::get().getSoundManager()->isMusicPlaying(); };
api["stopMusic"] = []() { MWBase::Environment::get().getSoundManager()->stopMusic(); };
return LuaUtil::makeReadOnly(api);
}
@ -103,6 +112,8 @@ namespace MWLua
sol::state_view& lua = context.mLua->sol();
sol::table api(lua, sol::create);
api["isEnabled"] = []() { return MWBase::Environment::get().getSoundManager()->isEnabled(); };
api["playSound3d"]
= [](std::string_view soundId, const Object& object, const sol::optional<sol::table>& options) {
auto args = getPlaySoundArgs(options);

@ -1285,7 +1285,7 @@ namespace MWMechanics
}
}
void Actors::updateCombatMusic()
bool Actors::playerHasHostiles() const
{
const MWWorld::Ptr player = getPlayer();
const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3();
@ -1315,19 +1315,7 @@ namespace MWMechanics
}
}
// check if we still have any player enemies to switch music
if (mCurrentMusic != MusicType::Explore && !hasHostiles
&& !(player.getClass().getCreatureStats(player).isDead()
&& MWBase::Environment::get().getSoundManager()->isMusicPlaying()))
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore"));
mCurrentMusic = MusicType::Explore;
}
else if (mCurrentMusic != MusicType::Battle && hasHostiles)
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle"));
mCurrentMusic = MusicType::Battle;
}
return hasHostiles;
}
void Actors::predictAndAvoidCollisions(float duration) const
@ -1735,8 +1723,6 @@ namespace MWMechanics
killDeadActors();
updateSneaking(playerCharacter, duration);
}
updateCombatMusic();
}
void Actors::notifyDied(const MWWorld::Ptr& actor)
@ -1806,7 +1792,8 @@ namespace MWMechanics
// player's death animation is over
MWBase::Environment::get().getStateManager()->askLoadRecent();
// Play Death Music if it was the player dying
MWBase::Environment::get().getSoundManager()->streamMusic("Special/MW_Death.mp3");
MWBase::Environment::get().getSoundManager()->streamMusic(
"Music/Special/MW_Death.mp3", MWSound::MusicType::Special);
}
else
{

@ -74,9 +74,6 @@ namespace MWMechanics
void dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore);
///< Deregister all actors (except for \a ignore) in the given cell.
void updateCombatMusic();
///< Update combat music state
void update(float duration, bool paused);
///< Update actor stats and store desired velocity vectors in \a movement
@ -159,19 +156,14 @@ namespace MWMechanics
bool isReadyToBlock(const MWWorld::Ptr& ptr) const;
bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const;
bool playerHasHostiles() const;
int getGreetingTimer(const MWWorld::Ptr& ptr) const;
float getAngleToPlayer(const MWWorld::Ptr& ptr) const;
GreetingState getGreetingState(const MWWorld::Ptr& ptr) const;
bool isTurningToPlayer(const MWWorld::Ptr& ptr) const;
private:
enum class MusicType
{
Title,
Explore,
Battle
};
std::map<ESM::RefId, int> mDeathCount;
std::list<Actor> mActors;
std::map<const MWWorld::LiveCellRefBase*, std::list<Actor>::iterator> mIndex;
@ -182,7 +174,6 @@ namespace MWMechanics
float mTimerUpdateHello = 0;
float mSneakTimer = 0; // Times update of sneak icon
float mSneakSkillTimer = 0; // Times sneak skill progress from "avoid notice"
MusicType mCurrentMusic = MusicType::Title;
void updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const;

@ -24,6 +24,7 @@
#include "../mwbase/dialoguemanager.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
@ -257,6 +258,7 @@ namespace MWMechanics
, mClassSelected(false)
, mRaceSelected(false)
, mAI(true)
, mMusicType(MWSound::MusicType::Special)
{
// buildPlayer no longer here, needs to be done explicitly after all subsystems are up and running
}
@ -340,6 +342,8 @@ namespace MWMechanics
mActors.update(duration, paused);
mObjects.update(duration, paused);
updateMusicState();
}
void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed)
@ -1572,6 +1576,31 @@ namespace MWMechanics
return (Misc::Rng::roll0to99(prng) >= target);
}
void MechanicsManager::updateMusicState()
{
bool musicPlaying = MWBase::Environment::get().getSoundManager()->isMusicPlaying();
// Can not interrupt scripted music by built-in playlists
if (mMusicType == MWSound::MusicType::Scripted && musicPlaying)
return;
const MWWorld::Ptr& player = MWMechanics::getPlayer();
bool hasHostiles = mActors.playerHasHostiles();
// check if we still have any player enemies to switch music
if (mMusicType != MWSound::MusicType::Explore && !hasHostiles
&& !(player.getClass().getCreatureStats(player).isDead() && musicPlaying))
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore"));
mMusicType = MWSound::MusicType::Explore;
}
else if (mMusicType != MWSound::MusicType::Battle && hasHostiles)
{
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Battle"));
mMusicType = MWSound::MusicType::Battle;
}
}
void MechanicsManager::startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target)
{
CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);

@ -11,6 +11,11 @@
#include "npcstats.hpp"
#include "objects.hpp"
namespace MWSound
{
enum class MusicType;
}
namespace MWWorld
{
class CellStore;
@ -33,6 +38,8 @@ namespace MWMechanics
typedef std::map<ESM::RefId, OwnerMap> StolenItemsMap;
StolenItemsMap mStolenItems;
MWSound::MusicType mMusicType;
public:
void buildPlayer();
///< build player according to stored class/race/birthsign information. Will
@ -232,7 +239,11 @@ namespace MWMechanics
GreetingState getGreetingState(const MWWorld::Ptr& ptr) const override;
bool isTurningToPlayer(const MWWorld::Ptr& ptr) const override;
MWSound::MusicType getMusicType() const override { return mMusicType; }
void setMusicType(MWSound::MusicType type) override { mMusicType = type; }
private:
void updateMusicState();
bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker);
bool canReportCrime(
const MWWorld::Ptr& actor, const MWWorld::Ptr& victim, std::set<MWWorld::Ptr>& playerFollowers);

@ -63,10 +63,11 @@ namespace MWScript
public:
void execute(Interpreter::Runtime& runtime) override
{
std::string sound{ runtime.getStringLiteral(runtime[0].mInteger) };
std::string music{ runtime.getStringLiteral(runtime[0].mInteger) };
runtime.pop();
MWBase::Environment::get().getSoundManager()->streamMusic(sound);
MWBase::Environment::get().getSoundManager()->streamMusic(
Misc::ResourceHelpers::correctMusicPath(music), MWSound::MusicType::Scripted);
}
};

@ -14,6 +14,7 @@
#include <components/vfs/pathutil.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/world.hpp"
@ -131,15 +132,6 @@ namespace MWSound
Log(Debug::Info) << stream.str();
}
// TODO: dehardcode this
std::vector<std::string> titleMusic;
std::string_view titlefile = "music/special/morrowind title.mp3";
if (mVFS->exists(titlefile))
titleMusic.emplace_back(titlefile);
else
Log(Debug::Warning) << "Title music not found";
mMusicFiles["Title"] = titleMusic;
}
SoundManager::~SoundManager()
@ -250,7 +242,7 @@ namespace MWSound
if (filename.empty())
return;
Log(Debug::Info) << "Playing " << filename;
Log(Debug::Info) << "Playing \"" << filename << "\"";
mLastPlayedMusic = filename;
DecoderPtr decoder = getDecoder();
@ -260,7 +252,7 @@ namespace MWSound
}
catch (std::exception& e)
{
Log(Debug::Error) << "Failed to load audio from " << filename << ": " << e.what();
Log(Debug::Error) << "Failed to load audio from \"" << filename << "\": " << e.what();
return;
}
@ -274,7 +266,7 @@ namespace MWSound
mOutput->streamSound(decoder, mMusic.get());
}
void SoundManager::advanceMusic(const std::string& filename)
void SoundManager::advanceMusic(const std::string& filename, float fadeOut)
{
if (!isMusicPlaying())
{
@ -284,7 +276,7 @@ namespace MWSound
mNextMusic = filename;
mMusic->setFadeout(1.f);
mMusic->setFadeout(fadeOut);
}
void SoundManager::startRandomTitle()
@ -319,14 +311,28 @@ namespace MWSound
tracklist.pop_back();
}
void SoundManager::streamMusic(const std::string& filename)
bool SoundManager::isMusicPlaying()
{
advanceMusic("Music/" + filename);
return mMusic && mOutput->isStreamPlaying(mMusic.get());
}
bool SoundManager::isMusicPlaying()
void SoundManager::streamMusic(const std::string& filename, MusicType type, float fade)
{
return mMusic && mOutput->isStreamPlaying(mMusic.get());
const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager();
// Can not interrupt scripted music by built-in playlists
if (mechanicsManager->getMusicType() == MusicType::Scripted && type != MusicType::Scripted
&& type != MusicType::Special)
return;
std::string normalizedName = VFS::Path::normalizeFilename(filename);
mechanicsManager->setMusicType(type);
advanceMusic(normalizedName, fade);
if (type == MWSound::MusicType::Battle)
mCurrentPlaylist = "Battle";
else if (type == MWSound::MusicType::Explore)
mCurrentPlaylist = "Explore";
}
void SoundManager::playPlaylist(const std::string& playlist)
@ -337,7 +343,8 @@ namespace MWSound
if (mMusicFiles.find(playlist) == mMusicFiles.end())
{
std::vector<std::string> filelist;
for (const auto& name : mVFS->getRecursiveDirectoryIterator("Music/" + playlist + '/'))
auto playlistPath = Misc::ResourceHelpers::correctMusicPath(playlist) + '/';
for (const auto& name : mVFS->getRecursiveDirectoryIterator(playlistPath))
filelist.push_back(name);
mMusicFiles[playlist] = filelist;
@ -1127,6 +1134,14 @@ namespace MWSound
if (!mOutput->isInitialized() || mPlaybackPaused)
return;
MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState();
if (state == MWBase::StateManager::State_NoGame && !isMusicPlaying())
{
std::string titlefile = "music/special/morrowind title.mp3";
if (mVFS->exists(titlefile))
streamMusic(titlefile, MWSound::MusicType::Special);
}
updateSounds(duration);
if (MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame)
{

@ -127,7 +127,7 @@ namespace MWSound
StreamPtr playVoice(DecoderPtr decoder, const osg::Vec3f& pos, bool playlocal);
void streamMusicFull(const std::string& filename);
void advanceMusic(const std::string& filename);
void advanceMusic(const std::string& filename, float fadeOut = 1.f);
void startRandomTitle();
void cull3DSound(SoundBase* sound);
@ -173,12 +173,17 @@ namespace MWSound
void processChangedSettings(const Settings::CategorySettingVector& settings) override;
bool isEnabled() const override { return mOutput->isInitialized(); }
///< Returns true if sound system is enabled
void stopMusic() override;
///< Stops music if it's playing
void streamMusic(const std::string& filename) override;
void streamMusic(const std::string& filename, MWSound::MusicType type, float fade = 1.f) override;
///< Play a soundifle
/// \param filename name of a sound file in "Music/" in the data directory.
/// \param filename name of a sound file in the data directory.
/// \param type music type.
/// \param fade time in seconds to fade out current track before start this one.
bool isMusicPlaying() override;
///< Returns true if music is playing

@ -391,7 +391,12 @@ namespace MWWorld
{
std::string_view video = Fallback::Map::getString("Movies_New_Game");
if (!video.empty())
{
// Make sure that we do not continue to play a Title music after a new game video.
MWBase::Environment::get().getSoundManager()->stopMusic();
MWBase::Environment::get().getSoundManager()->playPlaylist(std::string("Explore"));
MWBase::Environment::get().getWindowManager()->playVideo(video, true);
}
}
// enable collision

@ -156,6 +156,11 @@ std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath)
return "sound\\" + resPath;
}
std::string Misc::ResourceHelpers::correctMusicPath(const std::string& resPath)
{
return "music\\" + resPath;
}
std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath)
{
constexpr std::string_view prefix = "meshes";

@ -38,6 +38,9 @@ namespace Misc
// Adds "sound\\".
std::string correctSoundPath(const std::string& resPath);
// Adds "music\\".
std::string correctMusicPath(const std::string& resPath);
// Removes "meshes\\".
std::string_view meshPathForESM3(std::string_view resPath);

@ -72,4 +72,21 @@
-- @return #boolean
-- @usage local isPlaying = ambient.isSoundFilePlaying("Sound\\test.mp3");
---
-- Play a sound file as a music track
-- @function [parent=#ambient] streamMusic
-- @param #string fileName Path to file in VFS
-- @usage ambient.streamMusic("Music\\Test\\Test.mp3");
---
-- Stop to play current music
-- @function [parent=#ambient] stopMusic
-- @usage ambient.stopMusic();
---
-- Check if music is playing
-- @function [parent=#ambient] isMusicPlaying
-- @return #boolean
-- @usage local isPlaying = ambient.isMusicPlaying();
return nil

@ -749,6 +749,13 @@
--- @{#Sound}: Sounds and Speech
-- @field [parent=#core] #Sound sound
---
-- Checks if sound system is enabled (any functions to play sounds are no-ops when it is disabled).
-- It can not be enabled or disabled during runtime.
-- @function [parent=#Sound] isEnabled
-- @return #boolean
-- @usage local enabled = core.sound.isEnabled();
---
-- Play a 3D sound, attached to object
-- @function [parent=#Sound] playSound3d

Loading…
Cancel
Save