diff --git a/CMakeLists.txt b/CMakeLists.txt index 405df2d..1631dd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,9 @@ ccp_add_library(CarbonAudio SHARED src/AudGeometry.cpp src/AudGeometry_Blue.cpp src/AudObstructionOcclusion.cpp + src/AudRoom.cpp + src/AudRoom_Blue.cpp + src/AudRoomManager.cpp src/SpatialAudioSettings.cpp src/audio2.cpp src/AudGameObjResource_Blue.cpp @@ -86,6 +89,8 @@ target_sources(CarbonAudio PRIVATE src/AudMusicPlayer.h src/AudGeometry.h src/AudObstructionOcclusion.h + src/AudRoom.h + src/AudRoomManager.h src/SpatialAudioSettings.h src/autoversion.h src/DebugUtilities.h diff --git a/src/AudGameObjResource.cpp b/src/AudGameObjResource.cpp index a38f13e..d8bd6c6 100644 --- a/src/AudGameObjResource.cpp +++ b/src/AudGameObjResource.cpp @@ -10,12 +10,12 @@ #include "SoundPrioritization.h" //----------------------------------------------------------------------------- -// Helper function to distribute ID's +// Helper function to distribute ID's, shared with AudRoom //----------------------------------------------------------------------------- -static AkGameObjectID GenerateEntityID() +AkGameObjectID AllocateGameObjectID() { - static AkGameObjectID s_currentID = START_GAME_OBJ_COUNT; - return s_currentID++; + static std::atomic s_currentID{ START_GAME_OBJ_COUNT }; + return s_currentID.fetch_add( 1, std::memory_order_relaxed ); } AudGameObjResource::Orientation::Orientation( const Vector3& front_, const Vector3& top_ ) : @@ -49,7 +49,7 @@ AudGameObjResource::AudGameObjResource( IRoot* lockobj ) : PARENTLOCK( m_paramet m_waitingOneShotInRange( std::pair( std::chrono::steady_clock::now(), L"" ) ), m_eventName(L"") { - m_ID = GenerateEntityID(); + m_ID = AllocateGameObjectID(); if (g_audioManager != nullptr) { @@ -142,6 +142,7 @@ void AudGameObjResource::UnregisterWwiseObject() return; } m_gameObjRegistered = false; + g_audioManager->GetRoomManager().ForgetGameObject( m_ID ); } } @@ -463,6 +464,7 @@ int AudGameObjResource::ApplyEffectivePlacement( const Vector3& front, const Vec RH2LH::convertEmitter( &soundPosLH, &tmp ); AK::SoundEngine::SetPosition( m_ID, soundPosLH ); + g_audioManager->GetRoomManager().UpdateGameObjectPosition( m_ID, position ); } return AK_Success; } diff --git a/src/AudGeometry.cpp b/src/AudGeometry.cpp index e73f74c..d901e2f 100644 --- a/src/AudGeometry.cpp +++ b/src/AudGeometry.cpp @@ -45,6 +45,8 @@ AudGeometry::~AudGeometry() void AudGeometry::ClearAllGeometry() { + CcpAutoMutex lock( s_mutex ); + if( s_geometrySetRefCounts.empty() ) { return; @@ -89,6 +91,8 @@ void AudGeometry::SetGeometry( return; } + CcpAutoMutex lock( s_mutex ); + auto it = s_geometrySetRefCounts.find( geometrySetId ); if( it == s_geometrySetRefCounts.end() ) { @@ -143,6 +147,8 @@ void AudGeometry::SetGeometryTransform( return; } + CcpAutoMutex lock( s_mutex ); + if( s_geometrySetRefCounts.find( geometrySetId ) == s_geometrySetRefCounts.end() ) { return; @@ -161,6 +167,8 @@ void AudGeometry::RemoveGeometry( uint64_t geometrySetId, uint64_t instanceId ) { + CcpAutoMutex lock( s_mutex ); + auto it = s_geometrySetRefCounts.find( geometrySetId ); if( it == s_geometrySetRefCounts.end() ) { diff --git a/src/AudGeometry.h b/src/AudGeometry.h index c6159ff..4614fcb 100644 --- a/src/AudGeometry.h +++ b/src/AudGeometry.h @@ -13,6 +13,8 @@ #include +#include + /** * @brief Registers meshes from Trinity as Spatial Audio geometry sets and manages their lifecycle. @@ -85,7 +87,9 @@ BLUE_CLASS(AudGeometry) : uint64_t geometrySetId, const Matrix& worldTransform ); /// Tracks how many active instances reference each geometry set. + /// Guarded by s_mutex, trinity sends transforms from worker threads. inline static std::unordered_map s_geometrySetRefCounts; + inline static CcpMutex s_mutex{ "AudGeometry", "s_mutex" }; }; TYPEDEF_BLUECLASS( AudGeometry ); diff --git a/src/AudListener.cpp b/src/AudListener.cpp index bc5a8d8..e58cdad 100644 --- a/src/AudListener.cpp +++ b/src/AudListener.cpp @@ -16,12 +16,16 @@ AudListener::AudListener( IRoot* lockobj ) : AudGameObjResource( LISTENER_GAME_O AudListener::~AudListener() { - if( g_audioManager != nullptr && g_audioManager->GetSpatialAudioGeometryEnabled() ) + if( g_audioManager != nullptr && g_audioManager->IsSpatialAudioInitialized() ) { AK::SpatialAudio::UnregisterListener( m_ID ); } AK::SoundEngine::RemoveDefaultListener( m_ID ); AK::SoundEngine::UnregisterGameObj( m_ID ); + if( g_audioManager != nullptr ) + { + g_audioManager->GetRoomManager().ForgetGameObject( m_ID ); + } } void AudListener::RegisterWwiseObject() @@ -33,13 +37,8 @@ void AudListener::RegisterWwiseObject() AK::SoundEngine::RegisterGameObj(m_ID, m_name.c_str()); AK::SoundEngine::AddDefaultListener(m_ID); - // Register listener for occlusion/diffraction processing - if( g_audioManager != nullptr && g_audioManager->GetSpatialAudioGeometryEnabled() ) - { - AK::SpatialAudio::RegisterListener( m_ID ); - } - m_gameObjRegistered = true; + RegisterWithSpatialAudio(); } } else @@ -49,6 +48,14 @@ void AudListener::RegisterWwiseObject() } } +void AudListener::RegisterWithSpatialAudio() +{ + if( m_gameObjRegistered && g_audioManager != nullptr && g_audioManager->IsSpatialAudioInitialized() ) + { + AK::SpatialAudio::RegisterListener( m_ID ); + } +} + int AudListener::SetPlacementFromParent( const Vector3& front, const Vector3& top, const Vector3& position ) { if( g_audioManager != nullptr && g_audioManager->GetState() != AudioState::Uninitialized ) @@ -66,6 +73,7 @@ int AudListener::SetPlacementFromParent( const Vector3& front, const Vector3& to RH2LH::convertListener( &soundPosLH, &tmp ); AK::SoundEngine::SetPosition( m_ID, soundPosLH ); + g_audioManager->GetRoomManager().UpdateGameObjectPosition( m_ID, position ); } } return AK_Success; diff --git a/src/AudListener.h b/src/AudListener.h index 27a422e..83d0d98 100644 --- a/src/AudListener.h +++ b/src/AudListener.h @@ -25,6 +25,9 @@ BLUE_CLASS( AudListener ) : public AudGameObjResource void RegisterWwiseObject() override; int SetPlacementFromParent( const Vector3& front, const Vector3& top, const Vector3& position ) override; + + /// Registers the listener with Wwise Spatial Audio if both are initialized. + void RegisterWithSpatialAudio(); }; TYPEDEF_BLUECLASS( AudListener ); diff --git a/src/AudManager.cpp b/src/AudManager.cpp index 845f5cf..6e912f2 100644 --- a/src/AudManager.cpp +++ b/src/AudManager.cpp @@ -68,7 +68,7 @@ AudManager::AudManager( IRoot* lockobj ) : m_asyncOpen( true ), m_log(), m_spatialAudioEnabled( true ), - m_spatialAudioGeometryInitialized( false ), + m_spatialAudioInitialized( false ), m_moniteredParametersMapMutex( "AudManager", "m_monitoredParametersMapMutex" ), m_soundBankMutex( "AudManager", "m_soundBankMutex" ), m_callbackGameObjectsMutex( "AudManager", "m_callbackGameObjectsMutex" ), @@ -82,14 +82,11 @@ AudManager::AudManager( IRoot* lockobj ) : m_soundPrioritization = new SoundPrioritization(); m_spatialAudioSettings = new SpatialAudioSettings(); m_obstructionOcclusion = std::make_unique( this ); + m_roomManager = std::make_unique( this ); } AudManager::~AudManager() { - // Clean up sound prioritization system - delete m_soundPrioritization; - delete m_spatialAudioSettings; - if( GetState() == AudioState::Enabled ) { Disable(); @@ -102,6 +99,7 @@ AudManager::~AudManager() // Clean up sound prioritization system delete m_soundPrioritization; + delete m_spatialAudioSettings; } void AudManager::Process() @@ -120,6 +118,7 @@ void AudManager::Process() } m_obstructionOcclusion->Update(); + m_roomManager->Update(); // Process bank requests, events, positions, RTPC, etc. AK::SoundEngine::RenderAudio(); @@ -150,7 +149,7 @@ AkBankID AudManager::ComputeWwiseHashForSoundBank( const std::wstring& soundBank bool AudManager::Init() { - m_spatialAudioGeometryInitialized = false; + m_spatialAudioInitialized = false; if( g_staticDataRepository == nullptr || !g_staticDataRepository->IsInitialized() ) { @@ -170,15 +169,6 @@ bool AudManager::Init() return false; } - if( m_spatialAudioSettings->GetSpatialAudioGeometryEnabled() ) - { - if( !InitSpatialAudioGeometry() ) - { - CCP_LOGERR( "Failed to initialize Spatial Audio Geometry" ); - return false; - } - } - #ifndef AK_OPTIMIZED if( !InitCommunication() ) { @@ -221,8 +211,9 @@ void AudManager::Terminate() // Terminate the Memory Manager AK::MemoryMgr::Term(); + m_roomManager->ForgetWwiseState(); - m_spatialAudioGeometryInitialized = false; + m_spatialAudioInitialized = false; SetAudioState( AudioState::Uninitialized ); } @@ -446,9 +437,9 @@ bool AudManager::InitSound() return true; } -bool AudManager::InitSpatialAudioGeometry() +bool AudManager::InitSpatialAudio() { - if( m_spatialAudioGeometryInitialized ) + if( m_spatialAudioInitialized ) { return true; } @@ -458,12 +449,26 @@ bool AudManager::InitSpatialAudioGeometry() if( AK::SpatialAudio::Init( spatialSettings ) != AK_Success ) { - CCP_LOGERR( "Failed to initialize Wwise Spatial Audio for geometry processing" ); + CCP_LOGERR( "Failed to initialize Wwise Spatial Audio" ); + return false; + } + + m_spatialAudioInitialized = true; + CCP_LOG_CH( s_ch, "Wwise Spatial Audio initialized" ); + return true; +} + +bool AudManager::EnsureSpatialAudio() +{ + if( !InitSpatialAudio() ) + { return false; } - m_spatialAudioGeometryInitialized = true; - CCP_LOG_CH( s_ch, "Wwise Spatial Audio Geometry initialized" ); + if( AudListenerPtr listener = GetListener() ) + { + listener->RegisterWithSpatialAudio(); + } return true; } @@ -501,6 +506,24 @@ bool AudManager::GetSpatialAudioGeometryEnabled() const return m_spatialAudioSettings->GetSpatialAudioGeometryEnabled(); } +bool AudManager::GetSpatialAudioRoomsEnabled() const +{ + return m_spatialAudioSettings->GetSpatialAudioRoomsEnabled(); +} + +bool AudManager::UsesSpatialAudio() const +{ + return GetSpatialAudioGeometryEnabled() || GetSpatialAudioRoomsEnabled(); +} + +bool AudManager::AreRoomsReady() const +{ + return GetState() == AudioState::Enabled + && m_spatialAudioInitialized + && GetSpatialAudioRoomsEnabled() + && !g_shuttingDown; +} + void AudManager::SetSpatialAudioGeometryEnabled( bool enabled ) { const bool wasEnabled = GetSpatialAudioGeometryEnabled(); @@ -523,7 +546,7 @@ void AudManager::SetSpatialAudioGeometryEnabled( bool enabled ) } else { - if( !InitSpatialAudioGeometry() ) + if( !EnsureSpatialAudio() ) { CCP_LOGERR_CH( s_ch, "Spatial audio geometry failed to initialize." ); return; @@ -534,6 +557,39 @@ void AudManager::SetSpatialAudioGeometryEnabled( bool enabled ) } } +void AudManager::SetSpatialAudioRoomsEnabled( bool enabled ) +{ + if( GetSpatialAudioRoomsEnabled() == enabled ) + { + return; + } + + if( GetState() != AudioState::Enabled ) + { + m_spatialAudioSettings->SetSpatialAudioRoomsEnabled( enabled ); + return; + } + + if( !enabled ) + { + m_spatialAudioSettings->SetSpatialAudioRoomsEnabled( false ); + m_roomManager->RemoveAllFromWwise(); + CCP_LOG_CH( s_ch, "Spatial audio rooms disabled." ); + } + else + { + if( !EnsureSpatialAudio() ) + { + CCP_LOGERR_CH( s_ch, "Spatial audio rooms failed to initialize." ); + return; + } + + m_spatialAudioSettings->SetSpatialAudioRoomsEnabled( true ); + m_roomManager->ResendAll(); + CCP_LOG_CH( s_ch, "Spatial audio rooms enabled." ); + } +} + const bool AudManager::SpatialAudioIsSupported() { return s_systemSupportsSpatialAudio; @@ -837,7 +893,8 @@ void AudManager::Disable() ClearBanks(); m_obstructionQuery = nullptr; m_obstructionOcclusion->Reset(); - AudGeometry::ClearAllGeometry(); + m_roomManager->RemoveAllFromWwise(); + AudGeometry::ClearAllGeometry(); #ifndef AK_OPTIMIZED AK::SoundEngine::UnregisterResourceMonitorCallback(ResourceMonitorCallback); #endif @@ -923,6 +980,12 @@ void AudManager::Enable( BankVector soundBanksToLoad ) } SetAudioState( AudioState::Enabled ); + + if( UsesSpatialAudio() && !EnsureSpatialAudio() ) + { + CCP_LOGERR_CH( s_ch, "Spatial audio failed to initialize; geometry and rooms are inactive." ); + } + LoadBank( L"Init.bnk" ); BankVector::iterator bankEnd = soundBanksToLoad.end(); @@ -931,6 +994,8 @@ void AudManager::Enable( BankVector soundBanksToLoad ) LoadBank( *it ); } + m_roomManager->ResendAll(); + auto objects = m_soundPrioritization->GetPrioritizedAudioObjects(); for( auto obj : objects ) { @@ -1048,6 +1113,10 @@ AudGameObjResource* AudManager::GetAudioEmitter( AkGameObjectID emitterID ) const std::wstring AudManager::GetEventName( AkGameObjectID emitterID, AkPlayingID playingID ) { AudGameObjResource* emitter = GetAudioEmitter( emitterID ); + if( emitter == nullptr ) + { + return L""; + } std::map playingEvents = emitter->GetPlayingEvents(); auto it = playingEvents.find( playingID ); if( it != playingEvents.end() ) diff --git a/src/AudManager.h b/src/AudManager.h index 88c3f70..5d68eb4 100644 --- a/src/AudManager.h +++ b/src/AudManager.h @@ -10,6 +10,7 @@ #include "SoundPrioritization.h" #include "SpatialAudioSettings.h" #include "AudObstructionOcclusion.h" +#include "AudRoomManager.h" #include "LowLevelIO/LowLevelIOHook.h" #include #include @@ -124,6 +125,18 @@ BLUE_CLASS( AudManager ) : bool GetSpatialAudioGeometryEnabled() const; // Enables or disables spatial audio geometry. void SetSpatialAudioGeometryEnabled( bool enabled ); + // Returns whether Spatial Audio rooms are enabled. + bool GetSpatialAudioRoomsEnabled() const; + // Enables or disables Spatial Audio rooms. + void SetSpatialAudioRoomsEnabled( bool enabled ); + // Returns whether geometry or rooms need Wwise Spatial Audio. + bool UsesSpatialAudio() const; + // Returns whether Wwise Spatial Audio is initialized. + bool IsSpatialAudioInitialized() const { return m_spatialAudioInitialized; } + // Returns whether rooms can be sent to Wwise. + bool AreRoomsReady() const; + // Returns the room manager. + AudRoomManager& GetRoomManager() { return *m_roomManager; } // Set a single line-of-sight blockage ratio for an emitter [0.0, 1.0]. 0 = clear line of sight. bool SetEmitterLineOfSightBlockage( AkGameObjectID emitterID, float blockage ); // Current, mid-fade occlusion value for an emitter. 0.0 if the emitter is clear or not tracked. @@ -225,8 +238,10 @@ BLUE_CLASS( AudManager ) : bool InitMusic(); // Initializes Wwise's sound engine. bool InitSound(); - // Initializes Wwise's Spatial Audio for geometry-based occlusion and diffraction. - bool InitSpatialAudioGeometry(); + // Initializes Wwise Spatial Audio. + bool InitSpatialAudio(); + // Initializes Spatial Audio if needed and registers the listener with it. + bool EnsureSpatialAudio(); // Tick handler void Process(); // Registers audio2 for the tick handler. @@ -256,8 +271,8 @@ BLUE_CLASS( AudManager ) : bool m_asyncOpen; // Signals whether Carbon Audio's spatial audio features are enabled. If the user currently doesn't have an active spatial audio endpoint then output will still be in stereo. bool m_spatialAudioEnabled; - // Tracks whether Wwise Spatial Audio geometry has been initialized in the current audio-engine lifetime. - bool m_spatialAudioGeometryInitialized; + // Whether Wwise Spatial Audio is initialized. + bool m_spatialAudioInitialized; mutable bool m_audioCullingEnabled; std::map m_soundBankInfoMap; @@ -274,6 +289,7 @@ BLUE_CLASS( AudManager ) : SoundPrioritization* m_soundPrioritization; SpatialAudioSettings* m_spatialAudioSettings; std::unique_ptr m_obstructionOcclusion; + std::unique_ptr m_roomManager; // Map of game objects, used to guard Wwise callbacks std::unordered_map m_callbackGameObjects; diff --git a/src/AudManager_Blue.cpp b/src/AudManager_Blue.cpp index baae875..8662628 100644 --- a/src/AudManager_Blue.cpp +++ b/src/AudManager_Blue.cpp @@ -36,6 +36,7 @@ const Be::ClassInfo* AudManager::ExposeToBlue() // Spatial audio geometry settings MAP_PROPERTY( "spatialAudioGeometryEnabled", GetSpatialAudioGeometryEnabled, SetSpatialAudioGeometryEnabled, "Enable or disable spatial audio geometry.") + MAP_PROPERTY( "spatialAudioRoomsEnabled", GetSpatialAudioRoomsEnabled, SetSpatialAudioRoomsEnabled, "Enable or disable Spatial Audio rooms.") MAP_PROPERTY( "movementThreshold", GetMovementThreshold, SetMovementThreshold, "Distance an emitter or listener must move to trigger a re-validation of reflections/diffraction.") MAP_PROPERTY( "numberOfPrimaryRays", GetNumberOfPrimaryRays, SetNumberOfPrimaryRays, "Number of primary rays used in the ray tracing engine. More rays = better quality but higher CPU.") MAP_PROPERTY( "maxReflectionOrder", GetMaxReflectionOrder, SetMaxReflectionOrder, "Maximum reflection order [1-4] - number of bounces in a reflection path.") diff --git a/src/AudRoom.cpp b/src/AudRoom.cpp new file mode 100644 index 0000000..9247486 --- /dev/null +++ b/src/AudRoom.cpp @@ -0,0 +1,169 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudRoom.h" + +#include "AudManager.h" +#include "AudRoomManager.h" + +#include + +AudRoom::AudRoom( IRoot* lockobj ) : + m_roomID( AllocateGameObjectID() ), + m_reverbLevel( 1.0f ), + m_transmissionLoss( 1.0f ), + m_priority( 100.0f ), + m_auxSendLevelToSelf( 0.0f ), + m_keepRegistered( false ), + m_enabled( true ), + m_shapeEnabled( true ), + m_unitBoxToWorld( IdentityMatrix() ), + m_volume( 0.0f ), + m_shapeValid( false ), + m_warnedDegenerate( false ), + m_hasTransform( false ), + m_sentToWwise( false ), + m_registeredWithManager( false ), + m_roomTonePlayingID( AK_INVALID_PLAYING_ID ), + m_roomTonePending( false ) +{ + EnsureRegistered(); +} + +AudRoom::~AudRoom() +{ + if( g_audioManager != nullptr && m_registeredWithManager ) + { + g_audioManager->GetRoomManager().UnregisterRoom( this ); + } +} + +bool AudRoom::EnsureRegistered() +{ + if( m_registeredWithManager ) + { + return true; + } + + if( g_audioManager == nullptr ) + { + return false; + } + + g_audioManager->GetRoomManager().RegisterRoom( this ); + m_registeredWithManager = true; + return true; +} + +void AudRoom::Sync() +{ + if( !EnsureRegistered() ) + { + return; + } + + AudRoomManager& manager = g_audioManager->GetRoomManager(); + if( IsEnabled() && m_hasTransform && m_shapeValid ) + { + manager.Push( *this ); + } + else + { + manager.Remove( *this ); + } +} + +void AudRoom::SetTransform( const Matrix& unitBoxToWorld ) +{ + if( EnsureRegistered() ) + { + g_audioManager->GetRoomManager().SetTransform( *this, unitBoxToWorld ); + } + else + { + ApplyTransform( unitBoxToWorld ); + } +} + +void AudRoom::ApplyTransform( const Matrix& unitBoxToWorld ) +{ + m_unitBoxToWorld = unitBoxToWorld; + m_volume = std::fabs( Determinant( m_unitBoxToWorld ) ); + m_hasTransform = true; + + const Matrix& m = m_unitBoxToWorld; + constexpr float minAxisLengthSq = 1e-6f; + m_shapeValid = LengthSq( Vector3( m._11, m._12, m._13 ) ) > minAxisLengthSq + && LengthSq( Vector3( m._21, m._22, m._23 ) ) > minAxisLengthSq + && LengthSq( Vector3( m._31, m._32, m._33 ) ) > minAxisLengthSq; + if( !m_shapeValid && !m_warnedDegenerate ) + { + CCP_LOGWARN( "Room '%s' (%llu) has a box with a zero-length axis; it is not sent to Wwise until it is scaled.", m_name.c_str(), m_roomID ); + m_warnedDegenerate = true; + } +} + +bool AudRoom::ContainsPoint( const Vector3& worldPosition ) const +{ + if( !m_hasTransform || !m_shapeValid ) + { + return false; + } + + const Matrix& m = m_unitBoxToWorld; + const Vector3 axes[3] = + { + Vector3( m._11, m._12, m._13 ), + Vector3( m._21, m._22, m._23 ), + Vector3( m._31, m._32, m._33 ), + }; + const Vector3 offset = worldPosition - Vector3( m._41, m._42, m._43 ); + + for( const Vector3& axis : axes ) + { + const float lengthSq = LengthSq( axis ); + if( lengthSq <= 1e-12f ) + { + return false; + } + if( std::fabs( Dot( offset, axis ) ) > 0.5f * lengthSq ) + { + return false; + } + } + return true; +} + +void AudRoom::SetEnabled( bool enabled ) +{ + if( m_shapeEnabled == enabled ) + { + return; + } + + m_shapeEnabled = enabled; + Sync(); +} + +void AudRoom::Remove() +{ + if( EnsureRegistered() ) + { + g_audioManager->GetRoomManager().RemoveShape( *this ); + } + else + { + m_hasTransform = false; + } +} + +bool AudRoom::OnModified( Be::Var* value ) +{ + Sync(); + return true; +} diff --git a/src/AudRoom.h b/src/AudRoom.h new file mode 100644 index 0000000..f6bface --- /dev/null +++ b/src/AudRoom.h @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 CCP Games +// + +#pragma once + +#include + +#include +#include + +#include + +/** + * @brief A Wwise Spatial Audio room, placed as a unit box through ITr2BoxVolume. + */ +BLUE_CLASS( AudRoom ) : + public ITr2BoxVolume, + public INotify +{ +public: + AudRoom( IRoot* lockobj = NULL ); + virtual ~AudRoom(); + + EXPOSE_TO_BLUE(); + + // ITr2BoxVolume + void SetTransform( const Matrix& unitBoxToWorld ) override; + void SetEnabled( bool enabled ) override; + void Remove() override; + + // INotify + bool OnModified( Be::Var* value ) override; + + /// Room ID, from the same ID space as game objects. + AkUInt64 GetRoomID() const { return m_roomID; } + /// Name shown in the Wwise profiler. + const std::string& GetName() const { return m_name; } + /// Reverb aux bus name. Empty means no reverb. + const std::string& GetReverbAuxBus() const { return m_reverbAuxBus; } + float GetReverbLevel() const { return m_reverbLevel; } + float GetTransmissionLoss() const { return m_transmissionLoss; } + float GetPriority() const { return m_priority; } + float GetAuxSendLevelToSelf() const { return m_auxSendLevelToSelf; } + bool GetKeepRegistered() const { return m_keepRegistered; } + /// Room tone event. Empty means no room tone. + const std::wstring& GetRoomToneEvent() const { return m_roomToneEvent; } + /// True when the room and its volume object are both enabled. + bool IsEnabled() const { return m_enabled && m_shapeEnabled; } + bool HasTransform() const { return m_hasTransform; } + /// False when the box has a zero-length axis. + bool HasUsableShape() const { return m_shapeValid; } + const Matrix& GetUnitBoxToWorld() const { return m_unitBoxToWorld; } + /// Whether the room is in Wwise. + bool IsRegistered() const { return m_sentToWwise; } + + /// Returns whether the point is inside the room, in right-handed world space. + bool ContainsPoint( const Vector3& worldPosition ) const; + /// Volume of the box, the smaller room wins when priorities are equal. + float GetVolume() const { return m_volume; } + +private: + friend class AudRoomManager; + + /// Registers with the room manager once it exists. + bool EnsureRegistered(); + /// Sends the room to Wwise, or removes it when disabled. + void Sync(); + /// Stores the box. Called by AudRoomManager under its lock. + void ApplyTransform( const Matrix& unitBoxToWorld ); + + AkUInt64 m_roomID; + + std::string m_name; + std::string m_reverbAuxBus; + float m_reverbLevel; + float m_transmissionLoss; + float m_priority; + float m_auxSendLevelToSelf; + bool m_keepRegistered; + std::wstring m_roomToneEvent; + /// Enabled attribute of the room. + bool m_enabled; + /// Set through ITr2BoxVolume::SetEnabled. + bool m_shapeEnabled; + + Matrix m_unitBoxToWorld; + float m_volume; + bool m_shapeValid; + bool m_warnedDegenerate; + bool m_hasTransform; + bool m_sentToWwise; + bool m_registeredWithManager; + + /// Last parameters sent to Wwise. + AkRoomParams m_sentRoomParams; + std::string m_sentName; + + /// Room tone state, owned by AudRoomManager. + /// Room tone event last posted. Empty when none. + std::wstring m_postedRoomTone; + AkPlayingID m_roomTonePlayingID; + /// Room tone waiting for its SoundBanks. + bool m_roomTonePending; +}; + +TYPEDEF_BLUECLASS( AudRoom ); diff --git a/src/AudRoomManager.cpp b/src/AudRoomManager.cpp new file mode 100644 index 0000000..5615e39 --- /dev/null +++ b/src/AudRoomManager.cpp @@ -0,0 +1,589 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudRoomManager.h" + +#include "AudManager.h" +#include "AudRoom.h" +#include "AudStaticDataRepository.h" +#include "Utilities.h" + +#include +#include +#include + +static CcpLogChannel_t s_ch = CCP_LOG_DEFINE_CHANNEL( "AudRoomManager" ); + +namespace +{ + /// Unit cube [-0.5, 0.5]^3, the same in right- and left-handed space. + const AkVertex CUBE_VERTICES[8] = + { + AkVertex( -0.5f, -0.5f, -0.5f ), + AkVertex( 0.5f, -0.5f, -0.5f ), + AkVertex( 0.5f, 0.5f, -0.5f ), + AkVertex( -0.5f, 0.5f, -0.5f ), + AkVertex( -0.5f, -0.5f, 0.5f ), + AkVertex( 0.5f, -0.5f, 0.5f ), + AkVertex( 0.5f, 0.5f, 0.5f ), + AkVertex( -0.5f, 0.5f, 0.5f ), + }; + + const AkTriangle CUBE_TRIANGLES[12] = + { + AkTriangle( 0, 1, 2, 0 ), AkTriangle( 0, 2, 3, 0 ), + AkTriangle( 4, 6, 5, 0 ), AkTriangle( 4, 7, 6, 0 ), + AkTriangle( 0, 5, 1, 0 ), AkTriangle( 0, 4, 5, 0 ), + AkTriangle( 3, 2, 6, 0 ), AkTriangle( 3, 6, 7, 0 ), + AkTriangle( 0, 3, 7, 0 ), AkTriangle( 0, 7, 4, 0 ), + AkTriangle( 1, 5, 6, 0 ), AkTriangle( 1, 6, 2, 0 ), + }; +} + +namespace +{ + const AkUInt64 OUTDOOR_ROOM_ID = static_cast( AK::SpatialAudio::kOutdoorRoomID ); + + /// Compares orientations with a small tolerance for float errors. + bool OrientationsNearlyEqual( const AkVector& a, const AkVector& b ) + { + constexpr float epsilon = 1e-4f; + return std::fabs( a.X - b.X ) <= epsilon && std::fabs( a.Y - b.Y ) <= epsilon && std::fabs( a.Z - b.Z ) <= epsilon; + } + + /// Compares every parameter SetRoom uses. + bool RoomParamsEqual( const AkRoomParams& a, const AkRoomParams& b ) + { + return OrientationsNearlyEqual( a.Front, b.Front ) + && OrientationsNearlyEqual( a.Up, b.Up ) + && a.ReverbAuxBus == b.ReverbAuxBus + && a.ReverbLevel == b.ReverbLevel + && a.TransmissionLoss == b.TransmissionLoss + && a.RoomGameObj_AuxSendLevelToSelf == b.RoomGameObj_AuxSendLevelToSelf + && a.GeometryInstanceID == b.GeometryInstanceID + && a.RoomPriority == b.RoomPriority + && a.DistanceBehavior == b.DistanceBehavior + && a.RoomGameObj_KeepRegistered == b.RoomGameObj_KeepRegistered; + } +} + +AudRoomManager::AudRoomManager( AudManager* audioManager ) : + m_audioManager( audioManager ), + m_cubeSent( false ), + m_outdoorConfigured( false ), + m_assignmentsDirty( false ), + m_roomsInWwise( 0 ), + m_pendingRoomTones( 0 ), + m_mutex( "AudRoomManager", "m_mutex" ) +{ +} + +AudRoomManager::~AudRoomManager() +{ +} + +AkUInt64 AudRoomManager::GeometryInstanceIDForRoom( const AudRoom& room ) +{ + return room.GetRoomID() | ROOM_SPATIAL_ID_TAG; +} + +void AudRoomManager::RegisterRoom( AudRoom* room ) +{ + CcpAutoMutex lock( m_mutex ); + m_rooms[room->GetRoomID()] = room; +} + +void AudRoomManager::UnregisterRoom( AudRoom* room ) +{ + CcpAutoMutex lock( m_mutex ); + RemoveLocked( *room ); + m_rooms.erase( room->GetRoomID() ); +} + +size_t AudRoomManager::GetRoomCount() const +{ + CcpAutoMutex lock( m_mutex ); + return m_rooms.size(); +} + +size_t AudRoomManager::GetTrackedGameObjectCount() const +{ + CcpAutoMutex lock( m_mutex ); + return m_trackedObjects.size(); +} + +void AudRoomManager::UpdateGameObjectPosition( AkGameObjectID gameObjectID, const Vector3& position ) +{ + if( m_roomsInWwise.load( std::memory_order_acquire ) == 0 ) + { + return; + } + + CcpAutoMutex lock( m_mutex ); + if( m_roomsInWwise.load( std::memory_order_relaxed ) == 0 ) + { + return; + } + + TrackedGameObject& tracked = m_trackedObjects[gameObjectID]; + tracked.position = position; + AssignLocked( gameObjectID, tracked ); +} + +void AudRoomManager::ForgetGameObject( AkGameObjectID gameObjectID ) +{ + CcpAutoMutex lock( m_mutex ); + m_trackedObjects.erase( gameObjectID ); +} + +void AudRoomManager::Update() +{ + CcpAutoMutex lock( m_mutex ); + if( m_audioManager == nullptr || !m_audioManager->AreRoomsReady() ) + { + return; + } + + if( m_pendingRoomTones > 0 ) + { + for( auto& entry : m_rooms ) + { + AudRoom& room = *entry.second; + if( room.m_roomTonePending ) + { + const std::wstring eventName = room.m_postedRoomTone; + PostRoomToneLocked( room, eventName ); + } + } + } + + if( !m_assignmentsDirty ) + { + return; + } + + m_assignmentsDirty = false; + for( auto& entry : m_trackedObjects ) + { + AssignLocked( entry.first, entry.second ); + } +} + +AkUInt64 AudRoomManager::ResolveRoomLocked( const Vector3& position ) const +{ + const AudRoom* best = nullptr; + for( const auto& entry : m_rooms ) + { + const AudRoom* room = entry.second; + if( !room->m_sentToWwise || !room->ContainsPoint( position ) ) + { + continue; + } + + const float priority = room->m_sentRoomParams.RoomPriority; + const float bestPriority = best != nullptr ? best->m_sentRoomParams.RoomPriority : 0.0f; + if( best == nullptr + || priority > bestPriority + || ( priority == bestPriority && room->GetVolume() < best->GetVolume() ) ) + { + best = room; + } + } + + return best != nullptr ? best->GetRoomID() : OUTDOOR_ROOM_ID; +} + +void AudRoomManager::AssignLocked( AkGameObjectID gameObjectID, TrackedGameObject& tracked ) +{ + if( m_audioManager == nullptr || !m_audioManager->AreRoomsReady() ) + { + return; + } + + const AkUInt64 roomID = ResolveRoomLocked( tracked.position ); + + if( tracked.assigned && tracked.roomID == roomID ) + { + return; + } + + if( !tracked.assigned && roomID == OUTDOOR_ROOM_ID ) + { + return; + } + + const AKRESULT result = AK::SpatialAudio::SetGameObjectInRoom( gameObjectID, AkRoomID( roomID ) ); + if( result != AK_Success ) + { + CCP_LOGWARN_CH( s_ch, "Failed to set room %llu on game object %llu, AKRESULT: %d", roomID, gameObjectID, result ); + return; + } + + tracked.assigned = true; + tracked.roomID = roomID; +} + +bool AudRoomManager::EnsureSharedCubeGeometry() +{ + if( m_cubeSent ) + { + return true; + } + + AkAcousticSurface surface; + surface.strName = "AudRoomCube"; + surface.textureID = AK_INVALID_UNIQUE_ID; + surface.transmissionLoss = 1.0f; + + AkGeometryParams params; + params.Vertices = const_cast( CUBE_VERTICES ); + params.NumVertices = static_cast( std::size( CUBE_VERTICES ) ); + params.Triangles = const_cast( CUBE_TRIANGLES ); + params.NumTriangles = static_cast( std::size( CUBE_TRIANGLES ) ); + params.Surfaces = &surface; + params.NumSurfaces = 1; + params.EnableDiffraction = false; + params.EnableDiffractionOnBoundaryEdges = false; + + AKRESULT result = AK::SpatialAudio::SetGeometry( SHARED_CUBE_GEOMETRY_SET_ID, params ); + if( result != AK_Success ) + { + CCP_LOGERR_CH( s_ch, "Failed to set the shared room geometry, AKRESULT: %d", result ); + return false; + } + + m_cubeSent = true; + return true; +} + +void AudRoomManager::ReleaseSharedCubeGeometry() +{ + if( !m_cubeSent ) + { + return; + } + + AK::SpatialAudio::RemoveGeometry( SHARED_CUBE_GEOMETRY_SET_ID ); + m_cubeSent = false; +} + +bool AudRoomManager::EnsureOutdoorRoomConfigured() +{ + if( m_outdoorConfigured ) + { + return true; + } + + AkRoomParams params; + params.TransmissionLoss = 0.0f; + params.ReverbAuxBus = AK_INVALID_AUX_ID; + params.RoomGameObj_KeepRegistered = false; + params.GeometryInstanceID = AkGeometryInstanceID(); + + AKRESULT result = AK::SpatialAudio::SetRoom( AK::SpatialAudio::kOutdoorRoomID, params, "Outdoors" ); + if( result != AK_Success ) + { + CCP_LOGERR_CH( s_ch, "Failed to configure the outdoor room, AKRESULT: %d", result ); + return false; + } + + m_outdoorConfigured = true; + return true; +} + +void AudRoomManager::PushLocked( AudRoom& room ) +{ + if( !room.IsEnabled() || !room.HasTransform() || !room.HasUsableShape() ) + { + RemoveLocked( room ); + return; + } + + if( m_audioManager == nullptr || !m_audioManager->AreRoomsReady() ) + { + return; + } + + if( !EnsureSharedCubeGeometry() || !EnsureOutdoorRoomConfigured() ) + { + return; + } + + const AkUInt64 instanceID = GeometryInstanceIDForRoom( room ); + + AkTransform transform; + RH2LH::convertTransform( room.GetUnitBoxToWorld(), transform ); + + AkGeometryInstanceParams instanceParams; + instanceParams.PositionAndOrientation = transform; + instanceParams.Scale = RH2LH::extractScale( room.GetUnitBoxToWorld() ); + instanceParams.GeometrySetID = SHARED_CUBE_GEOMETRY_SET_ID; + instanceParams.UseForReflectionAndDiffraction = false; + instanceParams.BypassPortalSubtraction = false; + instanceParams.IsSolid = false; + + AKRESULT result = AK::SpatialAudio::SetGeometryInstance( instanceID, instanceParams ); + if( result != AK_Success ) + { + CCP_LOGERR_CH( s_ch, "Failed to set geometry instance for room '%s' (%llu), AKRESULT: %d", + room.GetName().c_str(), room.GetRoomID(), result ); + return; + } + + const std::wstring roomTone = StringUtils::trim( room.GetRoomToneEvent() ); + if( room.m_postedRoomTone != roomTone ) + { + StopRoomToneLocked( room ); + } + + AkRoomParams roomParams; + roomParams.Front = transform.OrientationFront(); + roomParams.Up = transform.OrientationTop(); + roomParams.ReverbAuxBus = room.GetReverbAuxBus().empty() + ? AK_INVALID_AUX_ID + : AK::SoundEngine::GetIDFromString( room.GetReverbAuxBus().c_str() ); + roomParams.ReverbLevel = std::clamp( room.GetReverbLevel(), 0.0f, 1.0f ); + roomParams.TransmissionLoss = std::clamp( room.GetTransmissionLoss(), 0.0f, 1.0f ); + roomParams.RoomGameObj_AuxSendLevelToSelf = std::clamp( room.GetAuxSendLevelToSelf(), 0.0f, 1.0f ); + roomParams.RoomGameObj_KeepRegistered = room.GetKeepRegistered() || !roomTone.empty(); + roomParams.GeometryInstanceID = instanceID; + roomParams.RoomPriority = room.GetPriority(); + + const bool firstSend = !room.m_sentToWwise; + const bool roomChanged = firstSend + || !RoomParamsEqual( roomParams, room.m_sentRoomParams ) + || room.GetName() != room.m_sentName; + + if( roomChanged ) + { + result = AK::SpatialAudio::SetRoom( room.GetRoomID(), roomParams, room.GetName().c_str() ); + if( result != AK_Success ) + { + CCP_LOGERR_CH( s_ch, "Failed to set room '%s' (%llu), AKRESULT: %d", + room.GetName().c_str(), room.GetRoomID(), result ); + if( firstSend ) + { + AK::SpatialAudio::RemoveGeometryInstance( instanceID ); + } + return; + } + + room.m_sentRoomParams = roomParams; + room.m_sentName = room.GetName(); + } + + if( firstSend ) + { + m_roomsInWwise.fetch_add( 1, std::memory_order_release ); + + if( room.GetReverbAuxBus().empty() ) + { + CCP_LOG_CH( s_ch, "Room '%s' (%llu) sent to Wwise without a reverb aux bus.", room.GetName().c_str(), room.GetRoomID() ); + } + else + { + CCP_LOG_CH( s_ch, "Room '%s' (%llu) sent to Wwise, reverb aux bus '%s' -> %u.", + room.GetName().c_str(), room.GetRoomID(), room.GetReverbAuxBus().c_str(), roomParams.ReverbAuxBus ); + } + } + + if( !roomTone.empty() && room.m_postedRoomTone != roomTone ) + { + PostRoomToneLocked( room, roomTone ); + } + + room.m_sentToWwise = true; + m_assignmentsDirty = true; +} + +void AudRoomManager::RemoveLocked( AudRoom& room ) +{ + if( !room.m_sentToWwise ) + { + return; + } + + StopRoomToneLocked( room ); + + room.m_sentToWwise = false; + m_assignmentsDirty = true; + const bool lastRoom = m_roomsInWwise.fetch_sub( 1, std::memory_order_acq_rel ) == 1; + const bool soundEngineUp = AK::SoundEngine::IsInitialized(); + + if( soundEngineUp ) + { + for( auto& entry : m_trackedObjects ) + { + TrackedGameObject& tracked = entry.second; + if( tracked.assigned && tracked.roomID == room.GetRoomID() ) + { + AK::SpatialAudio::UnsetGameObjectInRoom( entry.first ); + tracked.assigned = false; + } + } + + AK::SpatialAudio::RemoveRoom( room.GetRoomID() ); + AK::SpatialAudio::RemoveGeometryInstance( GeometryInstanceIDForRoom( room ) ); + } + + if( lastRoom ) + { + if( soundEngineUp ) + { + for( const auto& entry : m_trackedObjects ) + { + if( entry.second.assigned ) + { + AK::SpatialAudio::UnsetGameObjectInRoom( entry.first ); + } + } + } + m_trackedObjects.clear(); + } +} + +void AudRoomManager::StopRoomToneLocked( AudRoom& room ) +{ + if( room.m_roomTonePlayingID != AK_INVALID_PLAYING_ID && AK::SoundEngine::IsInitialized() ) + { + AK::SoundEngine::StopPlayingID( room.m_roomTonePlayingID ); + } + room.m_roomTonePlayingID = AK_INVALID_PLAYING_ID; + + if( room.m_roomTonePending ) + { + room.m_roomTonePending = false; + --m_pendingRoomTones; + } + room.m_postedRoomTone.clear(); +} + +void AudRoomManager::PostRoomToneLocked( AudRoom& room, const std::wstring& eventName ) +{ + room.m_postedRoomTone = eventName; + + if( g_staticDataRepository == nullptr ) + { + CCP_LOGERR_CH( s_ch, "Room '%s' (%llu): room tone %S cannot play without the static data repository.", + room.GetName().c_str(), room.GetRoomID(), eventName.c_str() ); + return; + } + + const std::vector& soundBanks = g_staticDataRepository->SoundBanksRequiredForEvent( eventName ); + if( soundBanks.empty() ) + { + CCP_LOGERR_CH( s_ch, "Room '%s' (%llu): room tone %S is not in any SoundBank.", + room.GetName().c_str(), room.GetRoomID(), eventName.c_str() ); + return; + } + + for( const std::wstring& soundBank : soundBanks ) + { + const SoundBankStatus status = m_audioManager->GetSoundBankStatus( soundBank ); + if( status != SoundBankStatus::LOADED ) + { + if( !room.m_roomTonePending ) + { + room.m_roomTonePending = true; + ++m_pendingRoomTones; + if( status != SoundBankStatus::LOADING ) + { + CCP_LOGWARN_CH( s_ch, "Room '%s' (%llu): room tone %S waits for SoundBank %S, which is not loaded.", + room.GetName().c_str(), room.GetRoomID(), eventName.c_str(), soundBank.c_str() ); + } + } + return; + } + } + + if( room.m_roomTonePending ) + { + room.m_roomTonePending = false; + --m_pendingRoomTones; + } + + const AkGameObjectID roomGameObjectID = AkRoomID( room.GetRoomID() ).AsGameObjectID(); + const AkUniqueID eventID = g_staticDataRepository->GetEventID( eventName ); + room.m_roomTonePlayingID = AK::SoundEngine::PostEvent( eventID, roomGameObjectID ); + m_audioManager->LogPostEvent( roomGameObjectID, room.m_roomTonePlayingID, eventID, eventName ); + + if( room.m_roomTonePlayingID == AK_INVALID_PLAYING_ID ) + { + CCP_LOGERR_CH( s_ch, "Room '%s' (%llu): room tone %S failed to play even though its SoundBanks are loaded.", + room.GetName().c_str(), room.GetRoomID(), eventName.c_str() ); + } +} + +void AudRoomManager::Push( AudRoom& room ) +{ + CcpAutoMutex lock( m_mutex ); + PushLocked( room ); +} + +void AudRoomManager::Remove( AudRoom& room ) +{ + CcpAutoMutex lock( m_mutex ); + RemoveLocked( room ); +} + +void AudRoomManager::SetTransform( AudRoom& room, const Matrix& unitBoxToWorld ) +{ + CcpAutoMutex lock( m_mutex ); + room.ApplyTransform( unitBoxToWorld ); + PushLocked( room ); +} + +void AudRoomManager::RemoveShape( AudRoom& room ) +{ + CcpAutoMutex lock( m_mutex ); + room.m_hasTransform = false; + RemoveLocked( room ); +} + +void AudRoomManager::RemoveAllFromWwise() +{ + CcpAutoMutex lock( m_mutex ); + for( auto& entry : m_rooms ) + { + RemoveLocked( *entry.second ); + } + + if( AK::SoundEngine::IsInitialized() ) + { + ReleaseSharedCubeGeometry(); + } + m_cubeSent = false; +} + +void AudRoomManager::ResendAll() +{ + CcpAutoMutex lock( m_mutex ); + for( auto& entry : m_rooms ) + { + PushLocked( *entry.second ); + } +} + +void AudRoomManager::ForgetWwiseState() +{ + CcpAutoMutex lock( m_mutex ); + for( auto& entry : m_rooms ) + { + AudRoom& room = *entry.second; + room.m_sentToWwise = false; + room.m_roomTonePlayingID = AK_INVALID_PLAYING_ID; + room.m_roomTonePending = false; + room.m_postedRoomTone.clear(); + } + m_pendingRoomTones = 0; + m_roomsInWwise.store( 0, std::memory_order_release ); + m_cubeSent = false; + m_outdoorConfigured = false; + m_trackedObjects.clear(); + m_assignmentsDirty = false; +} diff --git a/src/AudRoomManager.h b/src/AudRoomManager.h new file mode 100644 index 0000000..27002f5 --- /dev/null +++ b/src/AudRoomManager.h @@ -0,0 +1,113 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 CCP Games +// + +#pragma once + +#include + +#include +#include "Matrix.h" +#include "Vector3.h" + +#include +#include + +class AudManager; +class AudRoom; + +/** + * @brief Sends every AudRoom to Wwise and assigns game objects to rooms. + * + * Lock order: m_mutex can be held while taking AudManager's SoundBank and action-log locks, never the other way round. + */ +class AudRoomManager +{ +public: + explicit AudRoomManager( AudManager* audioManager ); + ~AudRoomManager(); + + /// Called by AudRoom on construction and destruction. + void RegisterRoom( AudRoom* room ); + void UnregisterRoom( AudRoom* room ); + + /// Creates or updates the room in Wwise. + void Push( AudRoom& room ); + /// Removes the room from Wwise. + void Remove( AudRoom& room ); + /// Stores the room's box under the lock and sends the room. + void SetTransform( AudRoom& room, const Matrix& unitBoxToWorld ); + /// Clears the room's box and removes the room from Wwise. + void RemoveShape( AudRoom& room ); + + /// Removes every room from Wwise, for Disable() and the rooms switch. + void RemoveAllFromWwise(); + /// Sends every enabled room to Wwise, for Enable() and the rooms switch. + void ResendAll(); + /// Resets the Wwise state after the sound engine is terminated. + void ForgetWwiseState(); + + /// Assigns the game object to the room it is in. Position is in right-handed world space. + void UpdateGameObjectPosition( AkGameObjectID gameObjectID, const Vector3& position ); + /// Removes a game object that was unregistered from Wwise. + void ForgetGameObject( AkGameObjectID gameObjectID ); + /// Assigns game objects again when rooms changed and retries room tones waiting for SoundBanks. + void Update(); + + size_t GetRoomCount() const; + size_t GetTrackedGameObjectCount() const; + +private: + struct TrackedGameObject + { + Vector3 position; + /// Room last sent with SetGameObjectInRoom. + AkUInt64 roomID; + /// Whether SetGameObjectInRoom was called for this object. + bool assigned; + + TrackedGameObject() : position( 0.0f, 0.0f, 0.0f ), roomID( 0 ), assigned( false ) {} + }; + + static constexpr AkUInt64 ROOM_SPATIAL_ID_TAG = 1ull << 62; + static constexpr AkUInt64 SHARED_CUBE_GEOMETRY_SET_ID = ROOM_SPATIAL_ID_TAG | 1ull; + + /// Geometry instance ID of the room, tagged so it can't collide with trinity's geometry IDs. + static AkUInt64 GeometryInstanceIDForRoom( const AudRoom& room ); + + /// Sends the shared unit cube once. Caller holds m_mutex. + bool EnsureSharedCubeGeometry(); + /// Removes the shared unit cube. Caller holds m_mutex. + void ReleaseSharedCubeGeometry(); + /// Sets the outdoor room to no transmission loss and no reverb. Caller holds m_mutex. + bool EnsureOutdoorRoomConfigured(); + /// Sends one room. Caller holds m_mutex. + void PushLocked( AudRoom& room ); + /// Removes one room with its room tone and game object assignments. Caller holds m_mutex. + void RemoveLocked( AudRoom& room ); + /// Stops the room tone. Caller holds m_mutex. + void StopRoomToneLocked( AudRoom& room ); + /// Plays the room tone, or waits for its SoundBanks. Caller holds m_mutex. + void PostRoomToneLocked( AudRoom& room, const std::wstring& eventName ); + + /// Returns the room containing the position, or the outdoor room. Caller holds m_mutex. + AkUInt64 ResolveRoomLocked( const Vector3& position ) const; + /// Calls SetGameObjectInRoom when the object's room changed. Caller holds m_mutex. + void AssignLocked( AkGameObjectID gameObjectID, TrackedGameObject& tracked ); + + AudManager* m_audioManager; + std::unordered_map m_rooms; + std::unordered_map m_trackedObjects; + bool m_cubeSent; + bool m_outdoorConfigured; + /// Set when rooms changed, so Update() assigns game objects again. + bool m_assignmentsDirty; + /// Number of rooms in Wwise, read without the lock. + std::atomic m_roomsInWwise; + /// Room tones waiting for SoundBanks. + size_t m_pendingRoomTones; + mutable CcpMutex m_mutex; +}; diff --git a/src/AudRoom_Blue.cpp b/src/AudRoom_Blue.cpp new file mode 100644 index 0000000..ec9b414 --- /dev/null +++ b/src/AudRoom_Blue.cpp @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Sep 2026 +// Copyright (c) 2026 CCP Games +// + +#include "stdafx.h" +#include "AudRoom.h" + +BLUE_DEFINE( AudRoom ); +BLUE_DEFINE_INTERFACE( ITr2BoxVolume ); + +const Be::ClassInfo* AudRoom::ExposeToBlue() +{ + EXPOSURE_BEGIN( AudRoom, "A Wwise Spatial Audio room, placed as a unit box through ITr2BoxVolume" ) + MAP_INTERFACE( ITr2BoxVolume ) + MAP_INTERFACE( INotify ) + + MAP_ATTRIBUTE( "name", m_name, "Name of the room, shown in the Wwise profiler.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "reverbAuxBus", m_reverbAuxBus, "Wwise auxiliary bus with the reverb of this room. Empty for no reverb.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "reverbLevel", m_reverbLevel, "Send level [0, 1] from emitters in the room to the reverb bus.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "transmissionLoss", m_transmissionLoss, "Transmission loss [0, 1] through the walls of this room. 1 blocks everything.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "priority", m_priority, "The room with the highest priority wins when rooms overlap.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "auxSendLevelToSelf", m_auxSendLevelToSelf, "Send level [0, 1] from the room tone to the reverb bus.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "keepRegistered", m_keepRegistered, "Keep the room game object registered in Wwise, needed for events and RTPCs on the room. Always on with a room tone.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "roomToneEvent", m_roomToneEvent, "Wwise event played on the room while it is in Wwise, e.g. an ambience bed.\n:jessica-group: Acoustics", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + MAP_ATTRIBUTE( "enabled", m_enabled, "Whether the room is active. It is only sent to Wwise when its volume object is enabled too.", Be::READWRITE | Be::PERSIST | Be::NOTIFY ) + + MAP_ATTRIBUTE( "roomID", m_roomID, "Wwise room identifier.\n:jessica-hidden: True", Be::READ ) + MAP_ATTRIBUTE( "isRegistered", m_sentToWwise, "Whether the room is in Wwise.\n:jessica-hidden: True", Be::READ ) + EXPOSURE_END() +} diff --git a/src/Audio2.h b/src/Audio2.h index 37df70e..6e20432 100644 --- a/src/Audio2.h +++ b/src/Audio2.h @@ -26,6 +26,9 @@ inline bool IsReservedGameObjectID( AkGameObjectID id ) return id < START_GAME_OBJ_COUNT; } +// Returns the next game object ID, also used for rooms. +AkGameObjectID AllocateGameObjectID(); + // Makes sure objects are initialized far away so you don't hear them when they spawn. // Game objects are culled by default so this value will never hit Wwise. const Vector3 WWISE_INIT_POSITION = Vector3(FLT_MAX, FLT_MAX, FLT_MAX); diff --git a/src/SpatialAudioSettings.cpp b/src/SpatialAudioSettings.cpp index d1332ff..c8f4c87 100644 --- a/src/SpatialAudioSettings.cpp +++ b/src/SpatialAudioSettings.cpp @@ -4,6 +4,7 @@ SpatialAudioSettings::SpatialAudioSettings() : m_spatialAudioGeometryEnabled( false ) + , m_spatialAudioRoomsEnabled( false ) , m_movementThreshold( 100.0f ) , m_numberOfPrimaryRays( 35 ) , m_maxReflectionOrder( 0 ) @@ -24,6 +25,9 @@ SpatialAudioSettings::SpatialAudioSettings() bool SpatialAudioSettings::GetSpatialAudioGeometryEnabled() const { return m_spatialAudioGeometryEnabled; } void SpatialAudioSettings::SetSpatialAudioGeometryEnabled( bool value ) { m_spatialAudioGeometryEnabled = value; } +bool SpatialAudioSettings::GetSpatialAudioRoomsEnabled() const { return m_spatialAudioRoomsEnabled; } +void SpatialAudioSettings::SetSpatialAudioRoomsEnabled( bool value ) { m_spatialAudioRoomsEnabled = value; } + float SpatialAudioSettings::GetMovementThreshold() const { return m_movementThreshold; } void SpatialAudioSettings::SetMovementThreshold( float value ) { m_movementThreshold = value; } diff --git a/src/SpatialAudioSettings.h b/src/SpatialAudioSettings.h index ab0d367..8e33bf6 100644 --- a/src/SpatialAudioSettings.h +++ b/src/SpatialAudioSettings.h @@ -24,6 +24,12 @@ class SpatialAudioSettings bool GetSpatialAudioGeometryEnabled() const; void SetSpatialAudioGeometryEnabled( bool value ); + /** + * @brief Controls whether Spatial Audio rooms are sent to Wwise. + */ + bool GetSpatialAudioRoomsEnabled() const; + void SetSpatialAudioRoomsEnabled( bool value ); + /** * @brief Amount that an emitter or listener has to move to trigger a validation of reflections/diffraction. * @@ -194,6 +200,7 @@ class SpatialAudioSettings private: bool m_spatialAudioGeometryEnabled; + bool m_spatialAudioRoomsEnabled; float m_movementThreshold; int m_numberOfPrimaryRays; int m_maxReflectionOrder; diff --git a/vcpkg.json b/vcpkg.json index 4ea3fab..1543dc9 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -10,7 +10,7 @@ }, { "name": "carbon-trinityaudioapi", - "version>=": "3.0.0" + "version>=": "3.1.0" }, { "name": "wwise",