0ad/source/soundmanager/SoundManager.cpp
bb 157c6af18e Make the space in 0 A.D. non-breaking throughout the codebase.
Avoid cases of filenames
Update years in terms and other legal(ish) documents
Don't update years in license headers, since change is not meaningful

Will add linter rule in seperate commit

Happy recompiling everyone!

Original Patch By: Nescio
Comment By: Gallaecio
Differential Revision: D2620
This was SVN commit r27786.
2023-07-27 20:54:46 +00:00

876 lines
18 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Copyright (C) 2021 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "ISoundManager.h"
#include "SoundManager.h"
#include "data/SoundData.h"
#include "items/CBufferItem.h"
#include "items/CSoundItem.h"
#include "items/CStreamItem.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Profiler2.h"
#include "ps/Threading.h"
#include "ps/XML/Xeromyces.h"
#include <thread>
ISoundManager* g_SoundManager = NULL;
#define SOURCE_NUM 64
#if CONFIG2_AUDIO
class CSoundManagerWorker
{
NONCOPYABLE(CSoundManagerWorker);
public:
CSoundManagerWorker()
{
m_Items = new ItemsList;
m_DeadItems = new ItemsList;
m_Shutdown = false;
m_WorkerThread = std::thread(Threading::HandleExceptions<RunThread>::Wrapper, this);
}
~CSoundManagerWorker()
{
delete m_Items;
CleanupItems();
delete m_DeadItems;
}
bool Shutdown()
{
{
std::lock_guard<std::mutex> lock(m_WorkerMutex);
m_Shutdown = true;
ItemsList::iterator lstr = m_Items->begin();
while (lstr != m_Items->end())
{
delete *lstr;
++lstr;
}
}
m_WorkerThread.join();
return true;
}
void addItem(ISoundItem* anItem)
{
std::lock_guard<std::mutex> lock(m_WorkerMutex);
m_Items->push_back(anItem);
}
void CleanupItems()
{
std::lock_guard<std::mutex> lock(m_DeadItemsMutex);
AL_CHECK;
ItemsList::iterator deadItems = m_DeadItems->begin();
while (deadItems != m_DeadItems->end())
{
delete *deadItems;
++deadItems;
AL_CHECK;
}
m_DeadItems->clear();
}
private:
static void RunThread(CSoundManagerWorker* data)
{
debug_SetThreadName("CSoundManagerWorker");
g_Profiler2.RegisterCurrentThread("soundmanager");
data->Run();
}
void Run()
{
while (true)
{
// Handle shutdown requests as soon as possible
if (GetShutdown())
return;
int pauseTime = 500;
if (g_SoundManager->InDistress())
pauseTime = 50;
{
std::lock_guard<std::mutex> workerLock(m_WorkerMutex);
ItemsList::iterator lstr = m_Items->begin();
ItemsList* nextItemList = new ItemsList;
while (lstr != m_Items->end())
{
AL_CHECK;
if ((*lstr)->IdleTask())
{
if ((pauseTime == 500) && (*lstr)->IsFading())
pauseTime = 100;
nextItemList->push_back(*lstr);
}
else
{
std::lock_guard<std::mutex> deadItemsLock(m_DeadItemsMutex);
m_DeadItems->push_back(*lstr);
}
++lstr;
AL_CHECK;
}
delete m_Items;
m_Items = nextItemList;
AL_CHECK;
}
SDL_Delay(pauseTime);
}
}
bool GetShutdown()
{
std::lock_guard<std::mutex> lock(m_WorkerMutex);
return m_Shutdown;
}
private:
// Thread-related members:
std::thread m_WorkerThread;
std::mutex m_WorkerMutex;
std::mutex m_DeadItemsMutex;
// Shared by main thread and worker thread:
// These variables are all protected by a mutexes
ItemsList* m_Items;
ItemsList* m_DeadItems;
bool m_Shutdown;
CSoundManagerWorker(ISoundManager* UNUSED(other)){};
};
void ISoundManager::CreateSoundManager()
{
if (g_SoundManager)
return;
ALCdevice* device = alcOpenDevice(nullptr);
if (!device)
{
LOGWARNING("No audio device was found.");
return;
}
g_SoundManager = new CSoundManager(device);
g_SoundManager->StartWorker();
}
void ISoundManager::SetEnabled(bool doEnable)
{
if (g_SoundManager && !doEnable)
SAFE_DELETE(g_SoundManager);
else if (!g_SoundManager && doEnable)
ISoundManager::CreateSoundManager();
}
void ISoundManager::CloseGame()
{
if (CSoundManager* aSndMgr = (CSoundManager*)g_SoundManager)
aSndMgr->SetAmbientItem(NULL);
}
void CSoundManager::al_ReportError(ALenum err, const char* caller, int line)
{
LOGERROR("OpenAL error: %s; called from %s (line %d)\n", alGetString(err), caller, line);
}
void CSoundManager::al_check(const char* caller, int line)
{
ALenum err = alGetError();
if (err != AL_NO_ERROR)
al_ReportError(err, caller, line);
}
Status CSoundManager::ReloadChangedFiles(const VfsPath& UNUSED(path))
{
// TODO implement sound file hotloading
return INFO::OK;
}
/*static*/ Status CSoundManager::ReloadChangedFileCB(void* param, const VfsPath& path)
{
return static_cast<CSoundManager*>(param)->ReloadChangedFiles(path);
}
CSoundManager::CSoundManager(ALCdevice* device)
: m_Context(nullptr), m_Device(device), m_ALSourceBuffer(nullptr),
m_CurrentTune(nullptr), m_CurrentEnvirons(nullptr),
m_Worker(nullptr), m_DistressMutex(), m_PlayListItems(nullptr), m_SoundGroups(),
m_Gain(.5f), m_MusicGain(.5f), m_AmbientGain(.5f), m_ActionGain(.5f), m_UIGain(.5f),
m_Enabled(false), m_BufferSize(98304), m_BufferCount(50),
m_SoundEnabled(true), m_MusicEnabled(true), m_MusicPaused(false),
m_AmbientPaused(false), m_ActionPaused(false),
m_RunningPlaylist(false), m_PlayingPlaylist(false), m_LoopingPlaylist(false),
m_PlaylistGap(0), m_DistressErrCount(0), m_DistressTime(0)
{
CFG_GET_VAL("sound.mastergain", m_Gain);
CFG_GET_VAL("sound.musicgain", m_MusicGain);
CFG_GET_VAL("sound.ambientgain", m_AmbientGain);
CFG_GET_VAL("sound.actiongain", m_ActionGain);
CFG_GET_VAL("sound.uigain", m_UIGain);
AlcInit();
if (m_Enabled)
{
SetMasterGain(m_Gain);
InitListener();
m_PlayListItems = new PlayList;
}
if (!CXeromyces::AddValidator(g_VFS, "sound_group", "audio/sound_group.rng"))
LOGERROR("CSoundManager: failed to load grammar file 'audio/sound_group.rng'");
RegisterFileReloadFunc(ReloadChangedFileCB, this);
RunHardwareDetection();
}
CSoundManager::~CSoundManager()
{
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
if (m_Worker)
{
AL_CHECK;
m_Worker->Shutdown();
AL_CHECK;
m_Worker->CleanupItems();
AL_CHECK;
delete m_Worker;
}
AL_CHECK;
for (const std::pair<const std::wstring, CSoundGroup*>& p : m_SoundGroups)
delete p.second;
m_SoundGroups.clear();
if (m_PlayListItems)
delete m_PlayListItems;
if (m_ALSourceBuffer != NULL)
delete[] m_ALSourceBuffer;
if (m_Context)
alcDestroyContext(m_Context);
if (m_Device)
alcCloseDevice(m_Device);
}
void CSoundManager::StartWorker()
{
if (m_Enabled)
m_Worker = new CSoundManagerWorker();
}
Status CSoundManager::AlcInit()
{
Status ret = INFO::OK;
if(!m_Device)
m_Device = alcOpenDevice(nullptr);
if (m_Device)
{
ALCint attribs[] = {ALC_STEREO_SOURCES, 16, 0};
m_Context = alcCreateContext(m_Device, &attribs[0]);
if (m_Context)
{
alcMakeContextCurrent(m_Context);
m_ALSourceBuffer = new ALSourceHolder[SOURCE_NUM];
ALuint* sourceList = new ALuint[SOURCE_NUM];
alGenSources(SOURCE_NUM, sourceList);
ALCenum err = alcGetError(m_Device);
if (err == ALC_NO_ERROR)
{
for (int x = 0; x < SOURCE_NUM; x++)
{
m_ALSourceBuffer[x].ALSource = sourceList[x];
m_ALSourceBuffer[x].SourceItem = NULL;
}
m_Enabled = true;
}
else
{
LOGERROR("error in gensource = %d", err);
}
delete[] sourceList;
}
}
// check if init succeeded.
// some OpenAL implementations don't indicate failure here correctly;
// we need to check if the device and context pointers are actually valid.
ALCenum err = alcGetError(m_Device);
const char* dev_name = (const char*)alcGetString(m_Device, ALC_DEVICE_SPECIFIER);
if (err == ALC_NO_ERROR && m_Device && m_Context)
debug_printf("Sound: AlcInit success, using %s\n", dev_name);
else
{
LOGERROR("Sound: AlcInit failed, m_Device=%p m_Context=%p dev_name=%s err=%x\n", (void *)m_Device, (void *)m_Context, dev_name, err);
// FIXME Hack to get around exclusive access to the sound device
#if OS_UNIX
ret = INFO::OK;
#else
ret = ERR::FAIL;
#endif // !OS_UNIX
}
return ret;
}
bool CSoundManager::InDistress()
{
std::lock_guard<std::mutex> lock(m_DistressMutex);
if (m_DistressTime == 0)
return false;
else if ((timer_Time() - m_DistressTime) > 10)
{
m_DistressTime = 0;
// Coming out of distress mode
m_DistressErrCount = 0;
return false;
}
return true;
}
void CSoundManager::SetDistressThroughShortage()
{
std::lock_guard<std::mutex> lock(m_DistressMutex);
// Going into distress for normal reasons
m_DistressTime = timer_Time();
}
void CSoundManager::SetDistressThroughError()
{
std::lock_guard<std::mutex> lock(m_DistressMutex);
// Going into distress due to unknown error
m_DistressTime = timer_Time();
m_DistressErrCount++;
}
ALuint CSoundManager::GetALSource(ISoundItem* anItem)
{
for (int x = 0; x < SOURCE_NUM; x++)
{
if (!m_ALSourceBuffer[x].SourceItem)
{
m_ALSourceBuffer[x].SourceItem = anItem;
return m_ALSourceBuffer[x].ALSource;
}
}
SetDistressThroughShortage();
return 0;
}
void CSoundManager::ReleaseALSource(ALuint theSource)
{
for (int x = 0; x < SOURCE_NUM; x++)
{
if (m_ALSourceBuffer[x].ALSource == theSource)
{
m_ALSourceBuffer[x].SourceItem = NULL;
return;
}
}
}
long CSoundManager::GetBufferCount()
{
return m_BufferCount;
}
long CSoundManager::GetBufferSize()
{
return m_BufferSize;
}
void CSoundManager::AddPlayListItem(const VfsPath& itemPath)
{
if (m_Enabled)
m_PlayListItems->push_back(itemPath);
}
void CSoundManager::ClearPlayListItems()
{
if (m_Enabled)
{
if (m_PlayingPlaylist)
SetMusicItem(NULL);
m_PlayingPlaylist = false;
m_LoopingPlaylist = false;
m_RunningPlaylist = false;
m_PlayListItems->clear();
}
}
void CSoundManager::StartPlayList(bool doLoop)
{
if (m_Enabled && m_MusicEnabled)
{
if (m_PlayListItems->size() > 0)
{
m_PlayingPlaylist = true;
m_LoopingPlaylist = doLoop;
m_RunningPlaylist = false;
ISoundItem* aSnd = LoadItem((m_PlayListItems->at(0)));
if (aSnd)
SetMusicItem(aSnd);
else
SetMusicItem(NULL);
}
}
}
void CSoundManager::SetMasterGain(float gain)
{
if (m_Enabled)
{
m_Gain = gain;
alListenerf(AL_GAIN, m_Gain);
AL_CHECK;
}
}
void CSoundManager::SetMusicGain(float gain)
{
m_MusicGain = gain;
if (m_CurrentTune)
m_CurrentTune->SetGain(m_MusicGain);
}
void CSoundManager::SetAmbientGain(float gain)
{
m_AmbientGain = gain;
}
void CSoundManager::SetActionGain(float gain)
{
m_ActionGain = gain;
}
void CSoundManager::SetUIGain(float gain)
{
m_UIGain = gain;
}
ISoundItem* CSoundManager::LoadItem(const VfsPath& itemPath)
{
AL_CHECK;
if (m_Enabled)
{
CSoundData* itemData = CSoundData::SoundDataFromFile(itemPath);
AL_CHECK;
if (itemData)
return CSoundManager::ItemForData(itemData);
}
return NULL;
}
ISoundItem* CSoundManager::ItemForData(CSoundData* itemData)
{
AL_CHECK;
ISoundItem* answer = NULL;
AL_CHECK;
if (m_Enabled && (itemData != NULL))
{
if (itemData->IsOneShot())
{
if (itemData->GetBufferCount() == 1)
answer = new CSoundItem(itemData);
else
answer = new CBufferItem(itemData);
}
else
{
answer = new CStreamItem(itemData);
}
if (answer && m_Worker)
m_Worker->addItem(answer);
}
return answer;
}
void CSoundManager::IdleTask()
{
if (m_Enabled)
{
if (m_CurrentTune)
{
m_CurrentTune->EnsurePlay();
if (m_PlayingPlaylist && m_RunningPlaylist)
{
if (m_CurrentTune->Finished())
{
if (m_PlaylistGap == 0)
{
m_PlaylistGap = timer_Time() + 15;
}
else if (m_PlaylistGap < timer_Time())
{
m_PlaylistGap = 0;
PlayList::iterator it = find(m_PlayListItems->begin(), m_PlayListItems->end(), m_CurrentTune->GetName());
if (it != m_PlayListItems->end())
{
++it;
Path nextPath;
if (it == m_PlayListItems->end())
nextPath = m_PlayListItems->at(0);
else
nextPath = *it;
ISoundItem* aSnd = LoadItem(nextPath);
if (aSnd)
SetMusicItem(aSnd);
}
}
}
}
}
if (m_CurrentEnvirons)
m_CurrentEnvirons->EnsurePlay();
if (m_Worker)
m_Worker->CleanupItems();
}
}
ISoundItem* CSoundManager::ItemForEntity(entity_id_t UNUSED(source), CSoundData* sndData)
{
ISoundItem* currentItem = NULL;
if (m_Enabled)
currentItem = ItemForData(sndData);
return currentItem;
}
void CSoundManager::InitListener()
{
ALfloat listenerPos[] = {0.0, 0.0, 0.0};
ALfloat listenerVel[] = {0.0, 0.0, 0.0};
ALfloat listenerOri[] = {0.0, 0.0, -1.0, 0.0, 1.0, 0.0};
alListenerfv(AL_POSITION, listenerPos);
alListenerfv(AL_VELOCITY, listenerVel);
alListenerfv(AL_ORIENTATION, listenerOri);
alDistanceModel(AL_LINEAR_DISTANCE);
}
void CSoundManager::PlayGroupItem(ISoundItem* anItem, ALfloat groupGain)
{
if (anItem)
{
if (m_Enabled && (m_ActionGain > 0))
{
anItem->SetGain(m_ActionGain * groupGain);
anItem->PlayAndDelete();
AL_CHECK;
}
}
}
void CSoundManager::SetMusicEnabled(bool isEnabled)
{
if (m_CurrentTune && !isEnabled)
{
m_CurrentTune->FadeAndDelete(1.00);
m_CurrentTune = NULL;
}
m_MusicEnabled = isEnabled;
}
void CSoundManager::PlayAsGroup(const VfsPath& groupPath, const CVector3D& sourcePos, entity_id_t source, bool ownedSound)
{
// Make sure the sound group is loaded
CSoundGroup* group;
if (m_SoundGroups.find(groupPath.string()) == m_SoundGroups.end())
{
group = new CSoundGroup();
if (!group->LoadSoundGroup(L"audio/" + groupPath.string()))
{
LOGERROR("Failed to load sound group '%s'", groupPath.string8());
delete group;
group = NULL;
}
// Cache the sound group (or the null, if it failed)
m_SoundGroups[groupPath.string()] = group;
}
else
{
group = m_SoundGroups[groupPath.string()];
}
// Failed to load group -> do nothing
if (group && (ownedSound || !group->TestFlag(eOwnerOnly)))
group->PlayNext(sourcePos, source);
}
void CSoundManager::PlayAsMusic(const VfsPath& itemPath, bool looping)
{
if (m_Enabled)
{
UNUSED2(looping);
ISoundItem* aSnd = LoadItem(itemPath);
if (aSnd != NULL)
SetMusicItem(aSnd);
}
}
void CSoundManager::PlayAsAmbient(const VfsPath& itemPath, bool looping)
{
if (m_Enabled)
{
UNUSED2(looping);
ISoundItem* aSnd = LoadItem(itemPath);
if (aSnd != NULL)
SetAmbientItem(aSnd);
}
}
void CSoundManager::PlayAsUI(const VfsPath& itemPath, bool looping)
{
if (m_Enabled)
{
IdleTask();
if (ISoundItem* anItem = LoadItem(itemPath))
{
if (m_UIGain > 0)
{
anItem->SetGain(m_UIGain);
anItem->SetLooping(looping);
anItem->PlayAndDelete();
}
}
AL_CHECK;
}
}
void CSoundManager::Pause(bool pauseIt)
{
PauseMusic(pauseIt);
PauseAmbient(pauseIt);
PauseAction(pauseIt);
}
void CSoundManager::PauseMusic(bool pauseIt)
{
if (m_CurrentTune && pauseIt && !m_MusicPaused)
{
m_CurrentTune->FadeAndPause(1.0);
}
else if (m_CurrentTune && m_MusicPaused && !pauseIt && m_MusicEnabled)
{
m_CurrentTune->SetGain(0);
m_CurrentTune->Resume();
m_CurrentTune->FadeToIn(m_MusicGain, 1.0);
}
m_MusicPaused = pauseIt;
}
void CSoundManager::PauseAmbient(bool pauseIt)
{
if (m_CurrentEnvirons && pauseIt)
m_CurrentEnvirons->Pause();
else if (m_CurrentEnvirons)
m_CurrentEnvirons->Resume();
m_AmbientPaused = pauseIt;
}
void CSoundManager::PauseAction(bool pauseIt)
{
m_ActionPaused = pauseIt;
}
void CSoundManager::SetMusicItem(ISoundItem* anItem)
{
if (m_Enabled)
{
AL_CHECK;
if (m_CurrentTune)
{
m_CurrentTune->FadeAndDelete(2.00);
m_CurrentTune = NULL;
}
IdleTask();
if (anItem)
{
if (m_MusicEnabled)
{
m_CurrentTune = anItem;
m_CurrentTune->SetGain(0);
if (m_PlayingPlaylist)
{
m_RunningPlaylist = true;
m_CurrentTune->Play();
}
else
m_CurrentTune->PlayLoop();
m_MusicPaused = false;
m_CurrentTune->FadeToIn(m_MusicGain, 1.00);
}
else
{
anItem->StopAndDelete();
}
}
AL_CHECK;
}
}
void CSoundManager::SetAmbientItem(ISoundItem* anItem)
{
if (m_Enabled)
{
if (m_CurrentEnvirons)
{
m_CurrentEnvirons->FadeAndDelete(3.00);
m_CurrentEnvirons = NULL;
}
IdleTask();
if (anItem)
{
if (m_AmbientGain > 0)
{
m_CurrentEnvirons = anItem;
m_CurrentEnvirons->SetGain(0);
m_CurrentEnvirons->PlayLoop();
m_CurrentEnvirons->FadeToIn(m_AmbientGain, 2.00);
}
}
AL_CHECK;
}
}
void CSoundManager::RunHardwareDetection()
{
// OpenAL alGetString might not return anything interesting on certain platforms
// (see https://stackoverflow.com/questions/28960638 for an example).
// However our previous code supported only Windows, and alGetString does work on
// Windows, so this is an improvement.
// Sound cards
const ALCchar* devices = nullptr;
if (alcIsExtensionPresent(nullptr, "ALC_enumeration_EXT") == AL_TRUE)
{
if (alcIsExtensionPresent(nullptr, "ALC_enumerate_all_EXT") == AL_TRUE)
devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
else
devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER);
}
WARN_IF_FALSE(devices);
m_SoundCardNames.clear();
do
{
m_SoundCardNames += devices;
devices += strlen(devices) + 1;
m_SoundCardNames += "; ";
} while (*devices);
// Driver version
const ALCchar* al_version = alGetString(AL_VERSION);
if (al_version)
m_OpenALVersion = al_version;
}
CStr8 CSoundManager::GetOpenALVersion() const
{
return m_OpenALVersion;
}
CStr8 CSoundManager::GetSoundCardNames() const
{
return m_SoundCardNames;
}
#else // CONFIG2_AUDIO
void ISoundManager::CreateSoundManager(){}
void ISoundManager::SetEnabled(bool UNUSED(doEnable)){}
void ISoundManager::CloseGame(){}
void ISoundManager::RunHardwareDetection() {}
CStr8 ISoundManager::GetSoundCardNames() const { return CStr8(); };
CStr8 ISoundManager::GetOpenALVersion() const { return CStr8(); };
#endif // CONFIG2_AUDIO