diff --git a/Client/game_sa/CAEAudioHardwareSA.cpp b/Client/game_sa/CAEAudioHardwareSA.cpp index 8eebfb1591..a1d7eb03a4 100644 --- a/Client/game_sa/CAEAudioHardwareSA.cpp +++ b/Client/game_sa/CAEAudioHardwareSA.cpp @@ -53,3 +53,244 @@ void CAEAudioHardwareSA::LoadSoundBank(short wSoundBankID, short wSoundBankSlotI } // clang-format on } + +namespace +{ + constexpr DWORD NUM_AudioHardwareBankLoaderOffset = 0xD98; + + constexpr DWORD NUM_AudioHardwareNumChannelsOffset = 0x8E; + constexpr DWORD NUM_AudioHardwareFreqScalingOffset = 0x310; + + constexpr DWORD NUM_BankLoaderSlotsOffset = 0x00; + constexpr DWORD NUM_BankLoaderBankLkupsOffset = 0x04; + constexpr DWORD NUM_BankLoaderSlotCountOffset = 0x0C; + constexpr DWORD NUM_BankLoaderBankLkupCntOffset = 0x0E; + constexpr DWORD NUM_BankLoaderBufferSizeOffset = 0x18; + constexpr DWORD NUM_BankLoaderBufferOffset = 0x1C; + + constexpr DWORD NUM_BankSlotSize = 0x12D4; + constexpr DWORD NUM_BankSlotOffsetBytesOffset = 0x00; + constexpr DWORD NUM_BankSlotNumBytesOffset = 0x04; + constexpr DWORD NUM_BankSlotBankIdOffset = 0x10; + constexpr DWORD NUM_BankSlotNumSoundsOffset = 0x12; + constexpr DWORD NUM_BankSlotSoundsArrayOffset = 0x14; + + constexpr DWORD NUM_BankSlotItemSize = 0x0C; + constexpr DWORD NUM_BankSlotItemBufferOffsetOffset = 0x00; + constexpr DWORD NUM_BankSlotItemLoopOffsetOffset = 0x04; + constexpr DWORD NUM_BankSlotItemSampleFreqOffset = 0x08; + + constexpr DWORD NUM_BankLookupSize = 0x0C; + constexpr DWORD NUM_BankLookupNumBytesOffset = 0x08; + + constexpr uint NUM_MaxBankSounds = 400; + + const BYTE* GetBankLoader() + { + const BYTE* pAudioHardware = reinterpret_cast(CLASS_CAEAudioHardware); + return *reinterpret_cast(pAudioHardware + NUM_AudioHardwareBankLoaderOffset); + } + + const BYTE* GetBankSlot(uint usBankSlot) + { + const BYTE* pBankLoader = GetBankLoader(); + if (!pBankLoader) + return nullptr; + + const ushort usSlotCount = *reinterpret_cast(pBankLoader + NUM_BankLoaderSlotCountOffset); + if (usBankSlot >= usSlotCount) + return nullptr; + + const BYTE* pBankSlots = *reinterpret_cast(pBankLoader + NUM_BankLoaderSlotsOffset); + if (!pBankSlots) + return nullptr; + + return pBankSlots + usBankSlot * NUM_BankSlotSize; + } + + const BYTE* GetBankSlotData(const BYTE* pBankSlot) + { + const BYTE* pBankLoader = GetBankLoader(); + if (!pBankLoader) + return nullptr; + + const BYTE* pBuffer = *reinterpret_cast(pBankLoader + NUM_BankLoaderBufferOffset); + if (!pBuffer) + return nullptr; + + const uint uiBufferSize = *reinterpret_cast(pBankLoader + NUM_BankLoaderBufferSizeOffset); + const uint uiOffsetBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotOffsetBytesOffset); + const uint uiNumBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotNumBytesOffset); + + if (uiOffsetBytes + uiNumBytes > uiBufferSize || uiNumBytes == 0) + return nullptr; + + return pBuffer + uiOffsetBytes; + } + + const BYTE* GetBankSlotItem(const BYTE* pBankSlot, uint usIndex) + { + return pBankSlot + NUM_BankSlotSoundsArrayOffset + usIndex * NUM_BankSlotItemSize; + } + + uint GetBankPcmSize(const BYTE* pBankSlot) + { + const BYTE* pBankLoader = GetBankLoader(); + if (!pBankLoader) + return 0; + + const BYTE* pBankLkups = *reinterpret_cast(pBankLoader + NUM_BankLoaderBankLkupsOffset); + if (!pBankLkups) + return 0; + + const ushort usBankLkupCnt = *reinterpret_cast(pBankLoader + NUM_BankLoaderBankLkupCntOffset); + const short sBankId = *reinterpret_cast(pBankSlot + NUM_BankSlotBankIdOffset); + if (sBankId < 0 || sBankId >= static_cast(usBankLkupCnt)) + return 0; + + return *reinterpret_cast(pBankLkups + sBankId * NUM_BankLookupSize + NUM_BankLookupNumBytesOffset); + } +} + +bool CAEAudioHardwareSA::GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, + unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const +{ + pOutPcmData = nullptr; + uiOutPcmSize = 0; + uiOutSampleRate = 0; + iOutLoopStartOffset = -1; + + const BYTE* pBankSlot = GetBankSlot(usBankSlot); + if (!pBankSlot) + return false; + + const short sNumSounds = *reinterpret_cast(pBankSlot + NUM_BankSlotNumSoundsOffset); + if (sNumSounds < 0 || usIndex >= static_cast(sNumSounds) || usIndex >= NUM_MaxBankSounds) + return false; + + const BYTE* pSlotData = GetBankSlotData(pBankSlot); + if (!pSlotData) + return false; + + const uint uiNumBytes = *reinterpret_cast(pBankSlot + NUM_BankSlotNumBytesOffset); + + const BYTE* pItem = GetBankSlotItem(pBankSlot, usIndex); + const uint uiBufferOffset = *reinterpret_cast(pItem + NUM_BankSlotItemBufferOffsetOffset); + + uint uiSize = 0; + if (usIndex + 1 < static_cast(sNumSounds)) + { + const uint uiNextOffset = *reinterpret_cast(GetBankSlotItem(pBankSlot, usIndex + 1) + NUM_BankSlotItemBufferOffsetOffset); + if (uiNextOffset <= uiBufferOffset) + return false; + uiSize = uiNextOffset - uiBufferOffset; + } + else + { + const uint uiBankPcmSize = GetBankPcmSize(pBankSlot); + if (uiBankPcmSize > 0) + { + if (uiBufferOffset >= uiBankPcmSize) + return false; + uiSize = uiBankPcmSize - uiBufferOffset; + } + else + { + if (uiBufferOffset >= uiNumBytes) + return false; + uiSize = uiNumBytes - uiBufferOffset; + } + } + + const BYTE* pPcmData = pSlotData + uiBufferOffset; + if (pPcmData + uiSize > pSlotData + uiNumBytes) + return false; + + pOutPcmData = const_cast(pPcmData); + uiOutPcmSize = uiSize; + uiOutSampleRate = *reinterpret_cast(pItem + NUM_BankSlotItemSampleFreqOffset); + iOutLoopStartOffset = *reinterpret_cast(pItem + NUM_BankSlotItemLoopOffsetOffset); + return true; +} + +uint CAEAudioHardwareSA::GetNumSoundsInBankSlot(unsigned short usBankSlot) const +{ + const BYTE* pBankSlot = GetBankSlot(usBankSlot); + if (!pBankSlot) + return 0; + + const short sNumSounds = *reinterpret_cast(pBankSlot + NUM_BankSlotNumSoundsOffset); + if (sNumSounds <= 0) + return 0; + + return static_cast(std::min(sNumSounds, NUM_MaxBankSounds)); +} + +void CAEAudioHardwareSA::GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const +{ + if (!pOutFactors || uiMax == 0) + return; + + const BYTE* pBase = reinterpret_cast(m_pInterface); + const ushort usNumChannels = *reinterpret_cast(pBase + NUM_AudioHardwareNumChannelsOffset); + const uint uiCount = std::min(uiMax, usNumChannels); + const float* pFactors = reinterpret_cast(pBase + NUM_AudioHardwareFreqScalingOffset); + + for (uint i = 0; i < uiCount; ++i) + pOutFactors[i] = pFactors[i]; +} + +bool CAEAudioHardwareSA::SetSoundSampleRate(unsigned short usBankSlot, unsigned short usIndex, unsigned short usSampleRate) +{ + const BYTE* pBankSlot = GetBankSlot(usBankSlot); + if (!pBankSlot) + return false; + + const short sNumSounds = *reinterpret_cast(pBankSlot + NUM_BankSlotNumSoundsOffset); + if (sNumSounds < 0 || usIndex >= static_cast(sNumSounds) || usIndex >= NUM_MaxBankSounds) + return false; + + BYTE* pItem = const_cast(GetBankSlotItem(pBankSlot, usIndex)); + if (!pItem) + return false; + + *reinterpret_cast(pItem + NUM_BankSlotItemSampleFreqOffset) = usSampleRate; + return true; +} + +bool CAEAudioHardwareSA::PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) +{ + void* pPcmDataDst = nullptr; + uint uiPcmSize = 0; + uint uiSampleRate = 0; + int iLoopStartOffset = -1; + + if (!GetLoadedSoundInfo(usBankSlot, usIndex, pPcmDataDst, uiPcmSize, uiSampleRate, iLoopStartOffset)) + return false; + + if (!pPcmData || uiDataSize > uiPcmSize) + return false; + + memcpy(pPcmDataDst, pPcmData, uiDataSize); + if (uiDataSize < uiPcmSize) + { + BYTE* pPad = static_cast(pPcmDataDst) + uiDataSize; + uint uiPadSize = uiPcmSize - uiDataSize; + if (iLoopStartOffset >= 0) + { + while (uiPadSize > 0) + { + const uint uiCopy = std::min(uiPadSize, uiDataSize); + memcpy(pPad, pPcmData, uiCopy); + pPad += uiCopy; + uiPadSize -= uiCopy; + } + } + else + { + memset(pPad, 0, uiPadSize); + } + } + + return true; +} diff --git a/Client/game_sa/CAEAudioHardwareSA.h b/Client/game_sa/CAEAudioHardwareSA.h index 8ab5f343da..a9ea87e011 100644 --- a/Client/game_sa/CAEAudioHardwareSA.h +++ b/Client/game_sa/CAEAudioHardwareSA.h @@ -29,6 +29,13 @@ class CAEAudioHardwareSA : public CAEAudioHardware bool IsSoundBankLoaded(short wSoundBankID, short wSoundBankSlotID); void LoadSoundBank(short wSoundBankID, short wSoundBankSlotID); + bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, unsigned int& uiOutSampleRate, + int& iOutLoopStartOffset) const override; + uint GetNumSoundsInBankSlot(unsigned short usBankSlot) const override; + bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) override; + bool SetSoundSampleRate(unsigned short usBankSlot, unsigned short usIndex, unsigned short usSampleRate) override; + void GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const override; + private: CAEAudioHardwareSAInterface* m_pInterface; }; diff --git a/Client/game_sa/CAESoundManagerSA.h b/Client/game_sa/CAESoundManagerSA.h index eab0919845..eae8716744 100644 --- a/Client/game_sa/CAESoundManagerSA.h +++ b/Client/game_sa/CAESoundManagerSA.h @@ -17,6 +17,7 @@ class CAESoundManagerSAInterface { +public: int16_t m_wNumAvailableChannels; // + 0x0000 // = CAEAudioHardware::GetNumAvailableChannels(...), [10, 300] int16_t m_wChannel; // + 0x0002 // = CAEAudioHardware::AllocateChannels(...), could be -1 CAESound m_aSound[300]; // + 0x0004 diff --git a/Client/game_sa/CAudioEngineSA.cpp b/Client/game_sa/CAudioEngineSA.cpp index c25464eb71..27b58b0e3e 100644 --- a/Client/game_sa/CAudioEngineSA.cpp +++ b/Client/game_sa/CAudioEngineSA.cpp @@ -14,6 +14,7 @@ #include "CAEAudioHardwareSA.h" #include "CAudioEngineSA.h" #include "CGameSA.h" +#include "CPadSA.h" #include "CPhysicalSA.h" #include "CSettingsSA.h" @@ -559,6 +560,9 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) pAESound->usIndex, pGameEntity, pAESound->m_vCurrPosn, + pAESound->m_fSoundDistance, + pAESound->usGroup == BANKSLOT_HORNS || pAESound->m_nLoopCounter < 0 || pAESound->m_nLoopCounter > 1, + pAESound, }; return m_pWorldSoundHandler(event); @@ -567,6 +571,50 @@ bool CAudioEngineSA::OnWorldSound(CAESound* pAESound) return true; } +void CAudioEngineSA::SetWorldSoundMaxDistance(CAESound* pAESound, float fMaxDistance) +{ + if (!pAESound || fMaxDistance <= 0.0f) + return; + + pAESound->m_fSoundDistance = fMaxDistance; +} + +bool CAudioEngineSA::IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const +{ + if (uiGroup == BANKSLOT_HORNS) + { + auto* pPad = dynamic_cast(pGame ? pGame->GetPad() : nullptr); + if (!pPad) + return false; + + const auto* pPadInterface = pPad->GetInterface(); + for (uint uiHistoryIndex = 0; uiHistoryIndex < MAX_HORN_HISTORY; ++uiHistoryIndex) + { + if (pPadInterface->bHornHistory[uiHistoryIndex]) + return true; + } + return false; + } + + const auto* pSoundManager = reinterpret_cast(CLASS_CAESoundManager); + if (!pSoundManager) + return false; + + for (const CAESound& sound : pSoundManager->m_aSound) + { + if (sound.m_nIsUsed != 0 && sound.usGroup == uiGroup && sound.usIndex == uiIndex) + { + if (!pEntity) + return true; + if (sound.pGameEntity == pEntity) + return true; + if (sound.pAudioEntity && sound.pAudioEntity->pEntity == pEntity) + return true; + } + } + return false; +} + //////////////////////////////////////////////////////////////////////////////////////////////// // Return false to skip sound static bool _cdecl OnRequestNewSound(CAESound* pAESound) diff --git a/Client/game_sa/CAudioEngineSA.h b/Client/game_sa/CAudioEngineSA.h index f86f5d8b0c..8fdee32d02 100644 --- a/Client/game_sa/CAudioEngineSA.h +++ b/Client/game_sa/CAudioEngineSA.h @@ -75,11 +75,9 @@ class CAESound float m_fCurrCamDist; // +72 float m_fPrevCamDist; // +76 float m_fTimeScale; // +80 - char unk2; // +84 = 31488 - char unk3; // +85 = 1005 union { - unsigned short m_wEnvironmentFlags; + unsigned short m_wEnvironmentFlags; // +84 struct { unsigned short m_bFrontEnd : 1; @@ -97,17 +95,18 @@ class CAESound unsigned short m_bForcedFront : 1; }; }; - unsigned short m_wIsUsed; // +88 - short unk4; // +90 = 1005 - short m_wCurrentPlayPosition; // +92 - short unk5; // +94 = 0 - float m_fFinalVolume; // +96 - float m_fFrequency; // +100 - short m_wPlayingState; // +104 - char unk6[2]; // +106 - float m_fSoundHeadRoom; // +108 - short m_wSoundLength; // +112 - short unk8; // +114 + short m_nLoopCounter; // +86 -1 = loop forever, >1 = replay N times, 0/1 = single play + short m_nMaxNumReplays; // +88 + short m_nIsUsed; // +90 1 while the sound pool slot is in use + short m_wCurrentPlayPosition; // +92 + short m_wLastPlayPosition; // +94 + float m_fFinalVolume; // +96 + float m_fFrequency; // +100 + short m_wPlayingState; // +104 + char unk6[2]; // +106 + float m_fSoundHeadRoom; // +108 + short m_wSoundLength; // +112 + short unk8; // +114 }; static_assert(sizeof(CAESound) == 0x74, "Invalid size for CAESound"); @@ -138,8 +137,10 @@ class CAudioEngineSA : public CAudioEngine bool IsWorldSoundEnabled(uint uiGroup, uint uiIndex); void ResetWorldSounds(); void SetWorldSoundHandler(WorldSoundHandler* pHandler); + void SetWorldSoundMaxDistance(CAESound* pAESound, float fMaxDistance); void ReportBulletHit(CEntity* pEntity, unsigned char ucSurfaceType, CVector* pvecPosition, float f_2); void ReportWeaponEvent(int iEvent, eWeaponType weaponType, CPhysical* pPhysical); + bool IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const; void UpdateAmbientSoundSettings(); bool OnWorldSound(CAESound* pAESound); diff --git a/Client/mods/deathmatch/logic/CClientGame.cpp b/Client/mods/deathmatch/logic/CClientGame.cpp index 6ed7e01717..b71da76060 100644 --- a/Client/mods/deathmatch/logic/CClientGame.cpp +++ b/Client/mods/deathmatch/logic/CClientGame.cpp @@ -40,6 +40,7 @@ #include #include "CServerInfo.h" #include "CClientPed.h" +#include "CClientWorldSoundManager.h" SString StringZeroPadout(const SString& strInput, uint uiPadoutSize) { @@ -199,6 +200,7 @@ CClientGame::CClientGame(bool bLocalPlay) : m_ServerInfo(new CServerInfo()) // Create the manager and grab the most important pointers m_pManager = new CClientManager; + m_pWorldSoundManager = new CClientWorldSoundManager(m_pManager); m_pCamera = m_pManager->GetCamera(); m_pMarkerManager = m_pManager->GetMarkerManager(); m_pObjectManager = m_pManager->GetObjectManager(); @@ -563,6 +565,7 @@ CClientGame::~CClientGame() // Destroy our stuff SAFE_DELETE(m_pManager); // Will trigger onClientResourceStop + SAFE_DELETE(m_pWorldSoundManager); SAFE_DELETE(m_pNametags); SAFE_DELETE(m_pSyncDebug); SAFE_DELETE(m_pNetworkStats); @@ -1558,6 +1561,11 @@ void CClientGame::DoPulses2(bool bCalledFromIdle) m_pManager->DoPulse(bDoStandardPulses, bDoVehicleManagerPulse); + if (m_pWorldSoundManager) + { + m_pWorldSoundManager->DoPulse(); + } + if (bDoStandardPulses) { m_pNetAPI->DoPulse(); @@ -6618,6 +6626,7 @@ bool CClientGame::WorldSoundHandler(const SWorldSoundEvent& event) if (!pEntity) pEntity = GetRootEntity(); + bool bAllowPlay = true; if (pEntity) { CLuaArguments Arguments; @@ -6626,10 +6635,13 @@ bool CClientGame::WorldSoundHandler(const SWorldSoundEvent& event) Arguments.PushNumber(event.vecPosition.fX); Arguments.PushNumber(event.vecPosition.fY); Arguments.PushNumber(event.vecPosition.fZ); - return pEntity->CallEvent("onClientWorldSound", Arguments, true); + bAllowPlay = pEntity->CallEvent("onClientWorldSound", Arguments, true); } - return true; + if (m_pWorldSoundManager) + m_pWorldSoundManager->HandleWorldSound(event); + + return bAllowPlay; } ////////////////////////////////////////////////////////////////// diff --git a/Client/mods/deathmatch/logic/CClientGame.h b/Client/mods/deathmatch/logic/CClientGame.h index f010e6d0c5..370ad89882 100644 --- a/Client/mods/deathmatch/logic/CClientGame.h +++ b/Client/mods/deathmatch/logic/CClientGame.h @@ -50,6 +50,7 @@ #define INVALID_DOWNLOAD_PRIORITY_GROUP (INT_MIN) class CClientModelCacheManager; +class CClientWorldSoundManager; class CDebugHookManager; class CResourceFileDownloadManager; class CServerInfo; @@ -277,6 +278,7 @@ class CClientGame // Accessors CVoiceRecorder* GetVoiceRecorder() { return m_pVoiceRecorder; }; + CClientWorldSoundManager* GetWorldSoundManager() { return m_pWorldSoundManager; }; CClientManager* GetManager() { return m_pManager; }; CClientObjectManager* GetObjectManager() { return m_pObjectManager; }; CClientPickupManager* GetPickupManager() { return m_pPickupManager; }; @@ -732,6 +734,7 @@ class CClientGame bool m_bFirstPlaybackFrame; CClientManager* m_pManager; + CClientWorldSoundManager* m_pWorldSoundManager; CClientCamera* m_pCamera; CClientGUIManager* m_pGUIManager; CClientMarkerManager* m_pMarkerManager; diff --git a/Client/mods/deathmatch/logic/CClientSoundManager.cpp b/Client/mods/deathmatch/logic/CClientSoundManager.cpp index 3f8c42d8a0..3131effdcb 100644 --- a/Client/mods/deathmatch/logic/CClientSoundManager.cpp +++ b/Client/mods/deathmatch/logic/CClientSoundManager.cpp @@ -319,6 +319,75 @@ void CClientSoundManager::UpdateVolume() BASS_SetConfig(BASS_CONFIG_GVOL_MUSIC, static_cast(fValue * 10000)); } +bool CClientSoundManager::ValidateSound(const SString& strSound, bool bIsRawData, SString* pOutError) +{ + HSTREAM hStream; + if (bIsRawData) + hStream = BASS_StreamCreateFile(true, strSound.data(), 0, static_cast(strSound.size()), BASS_STREAM_DECODE); + else + hStream = BASS_StreamCreateFile(false, FromUTF8(strSound), 0, 0, BASS_STREAM_DECODE | BASS_UNICODE); + + if (!hStream) + { + if (pOutError) + { + const int iError = BASS_ErrorGetCode(); + const char* szReason = "audio could not be decoded"; + switch (iError) + { + case BASS_ERROR_FILEOPEN: + szReason = "file cannot be opened (missing or access denied)"; + break; + case BASS_ERROR_FILEFORM: + szReason = "unsupported or corrupt audio format"; + break; + case BASS_ERROR_MEM: + szReason = "out of memory"; + break; + default: + break; + } + *pOutError = SString("%s (BASS error %d)", szReason, iError); + } + return false; + } + + BASS_StreamFree(hStream); + return true; +} + +bool CClientSoundManager::DecodeToPcm(const SString& strSound, bool bIsRawData, uint uiSampleRate, std::vector& outPcm) const +{ + outPcm.clear(); + + HSTREAM hStream; + if (bIsRawData) + hStream = BASS_StreamCreateFile(true, strSound.data(), 0, static_cast(strSound.size()), BASS_STREAM_DECODE | BASS_SAMPLE_MONO); + else + hStream = BASS_StreamCreateFile(false, FromUTF8(strSound), 0, 0, BASS_STREAM_DECODE | BASS_UNICODE | BASS_SAMPLE_MONO); + + if (!hStream) + return false; + + if (uiSampleRate > 0) + BASS_ChannelSetAttribute(hStream, BASS_ATTRIB_FREQ, static_cast(uiSampleRate)); + + const uint uiTotalBytes = static_cast(BASS_ChannelBytes2Seconds(hStream, BASS_ChannelGetLength(hStream, BASS_POS_BYTE)) * uiSampleRate * 2); + outPcm.reserve(uiTotalBytes); + + char buffer[8192]; + for (;;) + { + const DWORD dwRead = BASS_ChannelGetData(hStream, buffer, sizeof(buffer)); + if (dwRead == static_cast(-1) || dwRead == 0) + break; + outPcm.insert(outPcm.end(), buffer, buffer + dwRead); + } + + BASS_StreamFree(hStream); + return !outPcm.empty(); +} + // // Lists // diff --git a/Client/mods/deathmatch/logic/CClientSoundManager.h b/Client/mods/deathmatch/logic/CClientSoundManager.h index ff75cb7658..22770826bd 100644 --- a/Client/mods/deathmatch/logic/CClientSoundManager.h +++ b/Client/mods/deathmatch/logic/CClientSoundManager.h @@ -36,6 +36,10 @@ class CClientSoundManager bool GetSFXStatus(eAudioLookupIndex containerIndex); + bool ValidateSound(const SString& strSound, bool bIsRawData, SString* pOutError = nullptr); + + bool DecodeToPcm(const SString& strSound, bool bIsRawData, uint uiSampleRate, std::vector& outPcm) const; + void AddToList(CClientSound* pSound); void RemoveFromList(CClientSound* pSound); diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp new file mode 100644 index 0000000000..19c9441c91 --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.cpp @@ -0,0 +1,386 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/CClientWorldSoundManager.cpp + * PURPOSE: Client-side world sound replacement + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#include "StdInc.h" +#include "CClientWorldSoundManager.h" +#include "CClientGame.h" +#include "CClientManager.h" +#include "CClientSoundManager.h" +#include +#include + +CClientWorldSoundManager::CClientWorldSoundManager(CClientManager* pManager) : m_pManager(pManager) +{ +} + +CClientWorldSoundManager::~CClientWorldSoundManager() +{ + RestoreAll(); +} + +bool CClientWorldSoundManager::ReplaceSound(uint uiGroup, uint uiIndex, const SString& strSound, bool bIsRawData, float fMinDistance, float fMaxDistance, + SString* pOutError) +{ + if (uiIndex != static_cast(-1) && uiIndex > 399) + { + if (pOutError) + *pOutError = SString("invalid sound index %u (maximum is 399)", uiIndex); + return false; + } + + if (!std::isfinite(fMinDistance) || !std::isfinite(fMaxDistance)) + { + if (pOutError) + *pOutError = "minDistance/maxDistance must be finite numbers (no NaN or infinity)"; + return false; + } + if (fMinDistance > 0.0f && fMaxDistance > 0.0f && fMinDistance > fMaxDistance) + { + if (pOutError) + *pOutError = SString("minDistance (%.2f) cannot be greater than maxDistance (%.2f)", fMinDistance, fMaxDistance); + return false; + } + + if (!m_pManager->GetSoundManager()->ValidateSound(strSound, bIsRawData, pOutError)) + return false; + + SReplacement replacement; + replacement.bRawData = bIsRawData; + replacement.strSound = strSound; + replacement.fMinDistance = std::max(0.0f, fMinDistance); + replacement.fMaxDistance = std::max(0.0f, fMaxDistance); + replacement.bWholeGroup = (uiIndex == static_cast(-1)); + replacement.bNativeWanted = true; + replacement.uiNativeLastTryTick = 0; + + const uint uiKey = MakeKey(uiGroup, uiIndex); + auto iterExisting = m_Replacements.find(uiKey); + if (iterExisting != m_Replacements.end()) + { + replacement.originalPcm = std::move(iterExisting->second.originalPcm); + replacement.pcmByRate = std::move(iterExisting->second.pcmByRate); + } + + m_Replacements[uiKey] = std::move(replacement); + m_Replacements[uiKey].bNativeApplied = TryApplyNativeReplacement(m_Replacements[uiKey], uiGroup, uiIndex); + + return true; +} + +bool CClientWorldSoundManager::RestoreSound(uint uiGroup, uint uiIndex) +{ + if (uiIndex == static_cast(-1)) + { + bool bErased = false; + for (auto iter = m_Replacements.begin(); iter != m_Replacements.end();) + { + if ((iter->first >> 16) == uiGroup) + { + RestoreSoundBuffer(iter->second, uiGroup); + iter = m_Replacements.erase(iter); + bErased = true; + } + else + ++iter; + } + return bErased; + } + + auto iter = m_Replacements.find(MakeKey(uiGroup, uiIndex)); + if (iter == m_Replacements.end()) + return false; + + RestoreSoundBuffer(iter->second, uiGroup); + m_Replacements.erase(iter); + return true; +} + +void CClientWorldSoundManager::RestoreAll() +{ + if (g_pGame) + { + for (auto& iter : m_Replacements) + RestoreSoundBuffer(iter.second, iter.first >> 16); + } + m_Replacements.clear(); +} + +bool CClientWorldSoundManager::IsSoundReplaced(uint uiGroup, uint uiIndex) const +{ + if (uiIndex == static_cast(-1)) + { + for (const auto& iter : m_Replacements) + { + if ((iter.first >> 16) == uiGroup) + return true; + } + return false; + } + + const SReplacement* pReplacement = nullptr; + return FindReplacement(uiGroup, uiIndex, &pReplacement); +} + +bool CClientWorldSoundManager::FindReplacement(uint uiGroup, uint uiIndex, const SReplacement** ppOutReplacement) const +{ + auto iter = m_Replacements.find(MakeKey(uiGroup, uiIndex)); + if (iter != m_Replacements.end()) + { + *ppOutReplacement = &iter->second; + return true; + } + + iter = m_Replacements.find(MakeKey(uiGroup, static_cast(-1))); + if (iter != m_Replacements.end()) + { + *ppOutReplacement = &iter->second; + return true; + } + + return false; +} + +bool CClientWorldSoundManager::TryApplyNativeReplacement(SReplacement& replacement, uint uiGroup, uint uiIndex) +{ + if (!g_pGame) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + if (replacement.bWholeGroup) + { + const uint uiNumSounds = pAudioHardware->GetNumSoundsInBankSlot(static_cast(uiGroup)); + bool bAnyApplied = false; + for (uint i = 0; i < uiNumSounds; ++i) + { + if (PatchSoundBufferIndex(replacement, uiGroup, i)) + bAnyApplied = true; + } + return bAnyApplied; + } + + return PatchSoundBufferIndex(replacement, uiGroup, uiIndex); +} + +bool CClientWorldSoundManager::PatchSoundBufferIndex(SReplacement& replacement, uint uiGroup, uint uiIndex) +{ + if (!g_pGame) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + void* pPcmData = nullptr; + uint uiPcmSize = 0; + uint uiCurrentRate = 0; + int iLoopStartOffset = -1; + if (!pAudioHardware->GetLoadedSoundInfo(static_cast(uiGroup), static_cast(uiIndex), pPcmData, uiPcmSize, uiCurrentRate, iLoopStartOffset)) + return false; + + if (uiCurrentRate == 0) + { + LogResult(replacement, uiGroup, uiIndex, "invalid bank sample rate 0", false); + return false; + } + + const uint uiBaseRate = replacement.originalRate.contains(uiIndex) ? replacement.originalRate[uiIndex] : uiCurrentRate; + + std::vector& decoded = replacement.pcmByRate[uiBaseRate]; + if (decoded.empty() && !m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiBaseRate, decoded)) + { + LogResult(replacement, uiGroup, uiIndex, SString("failed to decode '%s'", *replacement.strSound), false); + return false; + } + + const uint uiSourceSize = static_cast(decoded.size()); + uint uiPatchRate = uiBaseRate; + + if (uiSourceSize > uiPcmSize) + { + if (iLoopStartOffset >= 0) + { + const uint uiRequiredRate = static_cast(uiBaseRate * (uiPcmSize * 0.98f) / uiSourceSize); + if (uiRequiredRate < 8000) + { + LogResult( + replacement, uiGroup, uiIndex, + SString("replacement too long for looping sound: needs %u Hz to fit %u bytes into %u byte slot", uiRequiredRate, uiSourceSize, uiPcmSize), + false); + return false; + } + + uiPatchRate = uiRequiredRate; + std::vector& fitted = replacement.pcmByRate[uiPatchRate]; + if (fitted.empty() && !m_pManager->GetSoundManager()->DecodeToPcm(replacement.strSound, replacement.bRawData, uiPatchRate, fitted)) + { + LogResult(replacement, uiGroup, uiIndex, SString("failed to decode '%s' at %u Hz", *replacement.strSound, uiPatchRate), false); + return false; + } + if (fitted.size() > uiPcmSize) + { + LogResult(replacement, uiGroup, uiIndex, + SString("replacement too long for looping sound: still %u bytes at %u Hz", static_cast(fitted.size()), uiPatchRate), false); + return false; + } + } + else + { + decoded.resize(uiPcmSize); + + const uint uiTotalSamples = uiPcmSize / 2; + const uint uiFadeSamples = std::min(uiTotalSamples, std::max(1, uiBaseRate / 125)); + short* pSamples = reinterpret_cast(decoded.data()); + const uint uiFadeStart = uiTotalSamples - uiFadeSamples; + for (uint i = 0; i < uiFadeSamples; ++i) + { + const float fGain = static_cast(uiFadeSamples - i) / static_cast(uiFadeSamples); + pSamples[uiFadeStart + i] = static_cast(static_cast(pSamples[uiFadeStart + i]) * fGain); + } + } + } + + if (!replacement.originalPcm.contains(uiIndex)) + { + replacement.originalPcm[uiIndex].assign(static_cast(pPcmData), static_cast(pPcmData) + uiPcmSize); + replacement.originalRate[uiIndex] = static_cast(uiCurrentRate); + } + + if (!pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), static_cast(uiIndex), replacement.pcmByRate[uiPatchRate].data(), + static_cast(replacement.pcmByRate[uiPatchRate].size()))) + { + LogResult(replacement, uiGroup, uiIndex, "failed to patch bank slot", false); + return false; + } + + if (uiPatchRate != uiCurrentRate && + !pAudioHardware->SetSoundSampleRate(static_cast(uiGroup), static_cast(uiIndex), static_cast(uiPatchRate))) + { + LogResult(replacement, uiGroup, uiIndex, "failed to set bank sample rate", false); + return false; + } + + if (uiSourceSize > uiPcmSize) + { + if (iLoopStartOffset >= 0) + LogResult(replacement, uiGroup, uiIndex, + SString("looping sound rate-lowered to fit: %u bytes at %u Hz (source was %u bytes at %u Hz)", uiPcmSize, uiPatchRate, uiSourceSize, + uiBaseRate), + true); + else + LogResult(replacement, uiGroup, uiIndex, + SString("sound trimmed to fit: %u bytes at %u Hz (source was %u bytes)", uiPcmSize, uiBaseRate, uiSourceSize), true); + } + + return true; +} + +bool CClientWorldSoundManager::RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup) +{ + if (replacement.originalPcm.empty() || !g_pGame) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + bool bRestored = false; + for (const auto& entry : replacement.originalPcm) + { + const ushort usIndex = static_cast(entry.first); + if (pAudioHardware->PatchSoundBuffer(static_cast(uiGroup), usIndex, entry.second.data(), static_cast(entry.second.size()))) + { + auto iterRate = replacement.originalRate.find(entry.first); + if (iterRate != replacement.originalRate.end()) + pAudioHardware->SetSoundSampleRate(static_cast(uiGroup), usIndex, iterRate->second); + bRestored = true; + } + } + return bRestored; +} + +void CClientWorldSoundManager::LogResult(SReplacement& replacement, uint uiGroup, uint uiIndex, const SString& strResult, bool bWarning) +{ + if (replacement.bResultLogged) + return; + + replacement.bResultLogged = true; + if (g_pClientGame && g_pClientGame->GetScriptDebugging()) + { + if (bWarning) + g_pClientGame->GetScriptDebugging()->LogWarning(NULL, "WorldSound: %s (group %u index %u)", *strResult, uiGroup, uiIndex); + else + g_pClientGame->GetScriptDebugging()->LogError(NULL, "WorldSound: %s (group %u index %u)", *strResult, uiGroup, uiIndex); + } +} + +void CClientWorldSoundManager::ApplyNativeReplacements() +{ + const uint uiNow = GetTickCount32(); + + for (auto& iter : m_Replacements) + { + SReplacement& replacement = iter.second; + if (!replacement.bNativeWanted) + continue; + + const uint uiStoredIndex = iter.first & 0xFFFF; + + if (!replacement.bNativeApplied) + { + if (uiNow - replacement.uiNativeLastTryTick < 500) + continue; + replacement.uiNativeLastTryTick = uiNow; + replacement.bNativeApplied = TryApplyNativeReplacement(replacement, iter.first >> 16, uiStoredIndex); + } + else if (uiNow - replacement.uiNativeLastTryTick > 2000) + { + replacement.uiNativeLastTryTick = uiNow; + TryApplyNativeReplacement(replacement, iter.first >> 16, uiStoredIndex); + } + } +} + +bool CClientWorldSoundManager::HandleWorldSound(const SWorldSoundEvent& event) +{ + SReplacement* pReplacement = nullptr; + auto iter = m_Replacements.find(MakeKey(event.uiGroup, event.uiIndex)); + if (iter != m_Replacements.end()) + pReplacement = &iter->second; + else + { + iter = m_Replacements.find(MakeKey(event.uiGroup, static_cast(-1))); + if (iter != m_Replacements.end()) + pReplacement = &iter->second; + } + + if (!pReplacement) + return false; + + const uint uiNow = GetTickCount32(); + if (!pReplacement->bNativeApplied && uiNow - pReplacement->uiNativeLastTryTick >= 500) + { + pReplacement->uiNativeLastTryTick = uiNow; + pReplacement->bNativeApplied = TryApplyNativeReplacement(*pReplacement, event.uiGroup, event.uiIndex); + } + + if (pReplacement->fMaxDistance > 0.0f && event.pAESound && g_pGame && g_pGame->GetAudioEngine()) + g_pGame->GetAudioEngine()->SetWorldSoundMaxDistance(event.pAESound, pReplacement->fMaxDistance); + + return false; +} + +void CClientWorldSoundManager::DoPulse() +{ + ApplyNativeReplacements(); +} diff --git a/Client/mods/deathmatch/logic/CClientWorldSoundManager.h b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h new file mode 100644 index 0000000000..c965c04ecb --- /dev/null +++ b/Client/mods/deathmatch/logic/CClientWorldSoundManager.h @@ -0,0 +1,66 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: mods/deathmatch/logic/CClientWorldSoundManager.h + * PURPOSE: Client-side world sound replacement + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * + *****************************************************************************/ + +#pragma once + +#include +#include +#include + +class CClientManager; + +class CClientWorldSoundManager +{ +public: + CClientWorldSoundManager(CClientManager* pManager); + ~CClientWorldSoundManager(); + + bool ReplaceSound(uint uiGroup, uint uiIndex, const SString& strSound, bool bIsRawData, float fMinDistance = 0.0f, float fMaxDistance = 0.0f, + SString* pOutError = nullptr); + bool RestoreSound(uint uiGroup, uint uiIndex); + void RestoreAll(); + + bool IsSoundReplaced(uint uiGroup, uint uiIndex) const; + bool HandleWorldSound(const SWorldSoundEvent& event); + + void DoPulse(); + +private: + struct SReplacement + { + bool bRawData; + SString strSound; + float fMinDistance; + float fMaxDistance; + + bool bWholeGroup = false; + bool bNativeWanted = false; + bool bNativeApplied = false; + bool bResultLogged = false; + std::unordered_map> pcmByRate; + std::unordered_map> originalPcm; + std::unordered_map originalRate; + uint uiNativeLastTryTick = 0; + }; + + static uint MakeKey(uint uiGroup, uint uiIndex) { return (uiGroup << 16) | (uiIndex & 0xFFFF); } + + bool FindReplacement(uint uiGroup, uint uiIndex, const SReplacement** ppOutReplacement) const; + + bool TryApplyNativeReplacement(SReplacement& replacement, uint uiGroup, uint uiIndex); + bool PatchSoundBufferIndex(SReplacement& replacement, uint uiGroup, uint uiIndex); + bool RestoreSoundBuffer(const SReplacement& replacement, uint uiGroup); + void ApplyNativeReplacements(); + void LogResult(SReplacement& replacement, uint uiGroup, uint uiIndex, const SString& strResult, bool bWarning); + + CClientManager* m_pManager; + std::unordered_map m_Replacements; +}; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp index 3feda344b8..139a18036a 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.cpp @@ -12,6 +12,8 @@ #include "StdInc.h" #include #include "CBassAudio.h" +#include "CClientWorldSoundManager.h" +#include void CLuaAudioDefs::LoadFunctions() { @@ -23,6 +25,11 @@ void CLuaAudioDefs::LoadFunctions() {"setWorldSoundEnabled", SetWorldSoundEnabled}, {"isWorldSoundEnabled", IsWorldSoundEnabled}, {"resetWorldSounds", ResetWorldSounds}, + {"replaceWorldSound", ArgumentParser}, + {"restoreWorldSound", ArgumentParser}, + {"restoreAllWorldSounds", ArgumentParser}, + {"isWorldSoundReplaced", ArgumentParser}, + {"getWorldSoundBankSlotInfo", ArgumentParser}, {"playSFX", PlaySFX}, {"playSFX3D", PlaySFX3D}, {"getSFXStatus", GetSFXStatus}, @@ -144,6 +151,75 @@ void CLuaAudioDefs::AddClass(lua_State* luaVM) lua_registerclass(luaVM, "Sound3D", "Sound"); } +bool CLuaAudioDefs::ReplaceWorldSound(lua_State* luaVM, std::string strSound, int group, std::optional index, std::optional fMinDistance, + std::optional fMaxDistance) +{ + CLuaMain& luaMain = lua_getownercluamain(luaVM); + CResource* pResource = luaMain.GetResource(); + if (!pResource || !g_pClientGame || !g_pClientGame->GetWorldSoundManager()) + return false; + + const int iIndex = index.value_or(-1); + SString strSoundCopy = strSound; + SString strFilename; + bool bIsRawData = false; + if (CResourceManager::ParseResourcePathInput(strSoundCopy, pResource, &strFilename, nullptr, true)) + strSoundCopy = strFilename; + else + bIsRawData = true; + + SString strError; + bool bSuccess = g_pClientGame->GetWorldSoundManager()->ReplaceSound(group, iIndex, strSoundCopy, bIsRawData, fMinDistance.value_or(-1.0f), + fMaxDistance.value_or(-1.0f), &strError); + if (!bSuccess && !strError.empty()) + m_pScriptDebugging->LogWarning(luaVM, "replaceWorldSound: %s (group %d, index %d, '%s')", strError.c_str(), group, iIndex, strSound.c_str()); + + return bSuccess; +} + +bool CLuaAudioDefs::RestoreWorldSound(int group, std::optional index) +{ + if (!g_pClientGame || !g_pClientGame->GetWorldSoundManager()) + return false; + + return g_pClientGame->GetWorldSoundManager()->RestoreSound(group, index.value_or(-1)); +} + +bool CLuaAudioDefs::RestoreAllWorldSounds() +{ + if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) + g_pClientGame->GetWorldSoundManager()->RestoreAll(); + + return true; +} + +bool CLuaAudioDefs::IsWorldSoundReplaced(int group, std::optional index) +{ + if (g_pClientGame && g_pClientGame->GetWorldSoundManager()) + return g_pClientGame->GetWorldSoundManager()->IsSoundReplaced(group, index.value_or(-1)); + + return false; +} + +std::variant> CLuaAudioDefs::GetWorldSoundBankSlotInfo(int group, int index) +{ + if (!g_pGame || group < 0 || group > 44 || index < 0 || index > 399) + return false; + + CAEAudioHardware* pAudioHardware = g_pGame->GetAEAudioHardware(); + if (!pAudioHardware) + return false; + + void* pPcmData = nullptr; + uint uiPcmSize = 0; + uint uiSampleRate = 0; + int iLoopStartOffset = -1; + if (!pAudioHardware->GetLoadedSoundInfo(static_cast(group), static_cast(index), pPcmData, uiPcmSize, uiSampleRate, iLoopStartOffset)) + return false; + + return std::tuple(uiPcmSize, uiSampleRate); +} + int CLuaAudioDefs::PlaySound(lua_State* luaVM) { SString strSound = ""; diff --git a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h index 2f9c63b3a3..400dd9e68f 100644 --- a/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h +++ b/Client/mods/deathmatch/logic/luadefs/CLuaAudioDefs.h @@ -11,6 +11,7 @@ #pragma once #include "CLuaDefs.h" +#include class CLuaAudioDefs : public CLuaDefs { @@ -26,6 +27,14 @@ class CLuaAudioDefs : public CLuaDefs LUA_DECLARE(SetWorldSoundEnabled); LUA_DECLARE(IsWorldSoundEnabled); LUA_DECLARE(ResetWorldSounds); + + static bool ReplaceWorldSound(lua_State* luaVM, std::string strSound, int group, std::optional index, std::optional fMinDistance, + std::optional fMaxDistance); + static bool RestoreWorldSound(int group, std::optional index); + static bool RestoreAllWorldSounds(); + static bool IsWorldSoundReplaced(int group, std::optional index); + static std::variant> GetWorldSoundBankSlotInfo(int group, int index); + LUA_DECLARE(PlaySFX); LUA_DECLARE(PlaySFX3D); LUA_DECLARE(GetSFXStatus); diff --git a/Client/sdk/game/CAEAudioHardware.h b/Client/sdk/game/CAEAudioHardware.h index 2061112317..1b085e32f8 100644 --- a/Client/sdk/game/CAEAudioHardware.h +++ b/Client/sdk/game/CAEAudioHardware.h @@ -65,4 +65,13 @@ class CAEAudioHardware public: virtual bool IsSoundBankLoaded(short wSoundBankID, short wSoundBankSlotID) = 0; virtual void LoadSoundBank(short wSoundBankID, short wSoundBankSlotID) = 0; + + virtual bool GetLoadedSoundInfo(unsigned short usBankSlot, unsigned short usIndex, void*& pOutPcmData, unsigned int& uiOutPcmSize, + unsigned int& uiOutSampleRate, int& iOutLoopStartOffset) const = 0; + + virtual uint GetNumSoundsInBankSlot(unsigned short usBankSlot) const = 0; + + virtual bool PatchSoundBuffer(unsigned short usBankSlot, unsigned short usIndex, const void* pPcmData, unsigned int uiDataSize) = 0; + virtual bool SetSoundSampleRate(unsigned short usBankSlot, unsigned short usIndex, unsigned short usSampleRate) = 0; + virtual void GetChannelFrequencyScalingFactors(float* pOutFactors, unsigned int uiMax) const = 0; }; diff --git a/Client/sdk/game/CAudioEngine.h b/Client/sdk/game/CAudioEngine.h index e91bea204b..f0e9bf50a7 100644 --- a/Client/sdk/game/CAudioEngine.h +++ b/Client/sdk/game/CAudioEngine.h @@ -19,6 +19,7 @@ class CEntity; class CEntitySAInterface; class CPhysical; class CVector; +class CAESound; struct SWorldSoundEvent { @@ -26,6 +27,9 @@ struct SWorldSoundEvent unsigned int uiIndex; CEntitySAInterface* pGameEntity; CVector vecPosition; + float fMaxDistance; + bool bLoop; + CAESound* pAESound; }; using WorldSoundHandler = bool(const SWorldSoundEvent& event); @@ -74,6 +78,9 @@ class CAudioEngine virtual bool IsWorldSoundEnabled(uint uiGroup, uint uiIndex) = 0; virtual void ResetWorldSounds() = 0; virtual void SetWorldSoundHandler(WorldSoundHandler* pHandler) = 0; + virtual void SetWorldSoundMaxDistance(CAESound* pAESound, float fMaxDistance) = 0; virtual void ReportBulletHit(CEntity* pEntity, unsigned char ucSurfaceType, CVector* pvecPosition, float f_2) = 0; virtual void ReportWeaponEvent(int iEvent, eWeaponType weaponType, CPhysical* pPhysical) = 0; + + virtual bool IsWorldSoundStillActive(uint uiGroup, uint uiIndex, CEntitySAInterface* pEntity) const = 0; };