From d17910c9778c8ad4c0fd4741d9be63d7cc42fe07 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 10:58:43 +0000 Subject: [PATCH 01/25] add empty AudObstructionOcclusion class implementation --- CMakeLists.txt | 2 ++ src/AudObstructionOcclusion.cpp | 10 ++++++++++ src/AudObstructionOcclusion.h | 11 +++++++++++ 3 files changed, 23 insertions(+) create mode 100644 src/AudObstructionOcclusion.cpp create mode 100644 src/AudObstructionOcclusion.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e82643..295a712 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ ccp_add_library(CarbonAudio SHARED src/AudGameObjResource.cpp src/AudGeometry.cpp src/AudGeometry_Blue.cpp + src/AudObstructionOcclusion.cpp src/SpatialAudioSettings.cpp src/audio2.cpp src/AudGameObjResource_Blue.cpp @@ -83,6 +84,7 @@ target_sources(CarbonAudio PRIVATE src/AudUIPlayer.h src/AudMusicPlayer.h src/AudGeometry.h + src/AudObstructionOcclusion.h src/SpatialAudioSettings.h src/autoversion.h src/DebugUtilities.h diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp new file mode 100644 index 0000000..19d9154 --- /dev/null +++ b/src/AudObstructionOcclusion.cpp @@ -0,0 +1,10 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Jul 2026 +// Copyright (c) 2026 CCP Games +// + +#include "AudObstructionOcclusion.h" +#include "AudManager.h" + diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h new file mode 100644 index 0000000..fbbc9d5 --- /dev/null +++ b/src/AudObstructionOcclusion.h @@ -0,0 +1,11 @@ +//////////////////////////////////////////////////////////// +// +// Creator: Phevos Rinis +// Creation Date: Jul 2026 +// Copyright (c) 2026 CCP Games +// + +#pragma once + +class AudObstructionOcclusion +{} \ No newline at end of file From 92791e1567c598ba74c1646a1bf3a003f9b21e24 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 11:24:50 +0000 Subject: [PATCH 02/25] create class skeleton layout --- src/AudObstructionOcclusion.cpp | 27 +++++++++++++++++++++++++++ src/AudObstructionOcclusion.h | 21 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 19d9154..f68644f 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -8,3 +8,30 @@ #include "AudObstructionOcclusion.h" #include "AudManager.h" +AudObstructionOcclusion::AudObstructionOcclusion() +{} + +AudObstructionOcclusion::~AudObstructionOcclusion() +{} + +void AudObstructionOcclusion::Update() +{} + +bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion) +{ + return false; +} + +bool AudObstructionOcclusion::SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage) +{ + return false; +} + +void AudObstructionOcclusion::RemoveEmitter(AkGameObjectID emitterID) +{} + +void AudObstructionOcclusion::Reset() +{} + +void AudObstructionOcclusion::ClearAll() +{} diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index fbbc9d5..e901caa 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -7,5 +7,24 @@ #pragma once +#include + class AudObstructionOcclusion -{} \ No newline at end of file +{ +public: + + AudObstructionOcclusion(); + ~AudObstructionOcclusion(); + + void Update(); + + bool SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion); + + bool SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage); + + void RemoveEmitter( AkGameObjectID emitterID ); + + void Reset(); + + void ClearAll(); +}; \ No newline at end of file From e03e8fc47967f2563fd095e832145a9dc94e170b Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 11:41:16 +0000 Subject: [PATCH 03/25] implement api call SetObjectObstructionAndOcclusion and structs for future fade values --- src/AudObstructionOcclusion.cpp | 10 ++++++++++ src/AudObstructionOcclusion.h | 16 ++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index f68644f..9c03865 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -27,6 +27,16 @@ bool AudObstructionOcclusion::SetEmitterLineOfSightBlockage(AkGameObjectID emitt return false; } +bool AudObstructionOcclusion::SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const +{ + const AKRESULT result = AK::SoundEngine::SetObjectObstructionAndOcclusion( + emitterID, + LISTENER_GAME_OBJ_ID, + state.obstruction.currentValue, + state.occlusion.currentValue); + return result == AK_Success; + + void AudObstructionOcclusion::RemoveEmitter(AkGameObjectID emitterID) {} diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index e901caa..3ba2295 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -9,6 +9,20 @@ #include +struct FadingValue +{ + float currentValue = 0.0f; + float targetValue = 0.0f; + float rate = 0.0f; +}; + +struct EmitterState +{ + FadingValue obstruction; + FadingValue occlusion; + +}; + class AudObstructionOcclusion { public: @@ -22,6 +36,8 @@ class AudObstructionOcclusion bool SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage); + bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const; + void RemoveEmitter( AkGameObjectID emitterID ); void Reset(); From 061c1076650d2c1f7bdc9d743fd35a4915fba3d8 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 13:41:45 +0000 Subject: [PATCH 04/25] implement SetObstructionOcclusion and improve structs for emitter state and fade values --- src/AudObstructionOcclusion.cpp | 27 ++++++++++++++++--- src/AudObstructionOcclusion.h | 47 ++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 9c03865..9aa97a9 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -8,7 +8,11 @@ #include "AudObstructionOcclusion.h" #include "AudManager.h" -AudObstructionOcclusion::AudObstructionOcclusion() +AudObstructionOcclusion::AudObstructionOcclusion() : + m_fadeRate(0.0f), + m_enabled(false), + m_hasUpdated(false), + m_mutex("AudObstructionOcclusion;", "m_mutex") {} AudObstructionOcclusion::~AudObstructionOcclusion() @@ -19,7 +23,19 @@ void AudObstructionOcclusion::Update() bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion) { - return false; + // We need to ask AudioManager about emitters that actually exist. + if (!g_audioManager->WithCallbackGameObject(emitterID, [](AudGameObjResource*) {})) + { + return false; + } + + CcpAutoMutex lock(m_mutex); + EmitterState& state = m_emitters[emitterID]; + + state.obstruction.SetTarget(obstruction, m_fadeRate); + state.occlusion.SetTarget(occlusion, m_fadeRate); + + return true; } bool AudObstructionOcclusion::SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage) @@ -35,7 +51,7 @@ bool AudObstructionOcclusion::SendToWwise(AkGameObjectID emitterID, const Emitte state.obstruction.currentValue, state.occlusion.currentValue); return result == AK_Success; - +} void AudObstructionOcclusion::RemoveEmitter(AkGameObjectID emitterID) {} @@ -45,3 +61,8 @@ void AudObstructionOcclusion::Reset() void AudObstructionOcclusion::ClearAll() {} + +void AudObstructionOcclusion::FadingValue::SetTarget(float target, float fadeRate) +{ + +} diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 3ba2295..41c1394 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -9,20 +9,6 @@ #include -struct FadingValue -{ - float currentValue = 0.0f; - float targetValue = 0.0f; - float rate = 0.0f; -}; - -struct EmitterState -{ - FadingValue obstruction; - FadingValue occlusion; - -}; - class AudObstructionOcclusion { public: @@ -36,11 +22,40 @@ class AudObstructionOcclusion bool SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage); - bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const; - void RemoveEmitter( AkGameObjectID emitterID ); void Reset(); void ClearAll(); + +private: + + struct FadingValue + { + float currentValue = 0.0f; + float targetValue = 0.0f; + float rate = 0.0f; + + void SetTarget(float target, float fadeRate); + bool Advance(float deltaSeconds); + bool ReachedTarget() const { return currentValue == targetValue; }; + }; + + struct EmitterState + { + FadingValue obstruction; + FadingValue occlusion; + + bool needsSend = true; + + }; + + bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const; + + std::unordered_map m_emitters; + float m_fadeRate; + bool m_enabled; + bool m_hasUpdated; + mutable CcpMutex m_mutex; + }; \ No newline at end of file From b1682c3d47634a0e45ad64fc605d04ee51ae4ff0 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 13:52:11 +0000 Subject: [PATCH 05/25] implement setTarget and Advance logic --- src/AudObstructionOcclusion.cpp | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 9aa97a9..1d483ac 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -64,5 +64,41 @@ void AudObstructionOcclusion::ClearAll() void AudObstructionOcclusion::FadingValue::SetTarget(float target, float fadeRate) { + targetValue = std::clamp(target, 0.0f, 1.0f); + if (targetValue >= currentValue) + { + rate = fadeRate; + } + else + { + rate = -fadeRate; + } +} + +bool AudObstructionOcclusion::FadingValue::Advance(float deltaSeconds) +{ + if (currentValue == targetValue) + { + return false; + } + + if (rate == 0.0f) + { + currentValue = targetValue; + return true; + } + + const float oldValue = currentValue; + const float newValue = oldValue + rate * deltaSeconds; + + if (oldValue > targetValue) + { + currentValue = std::clamp(newValue, targetValue, oldValue); + } + else + { + currentValue = std::clamp(newValue, oldValue, targetValue); + } + return currentValue != oldValue; } From eded50af53980d09e732667e78e782ade2691265 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:12:36 +0000 Subject: [PATCH 06/25] write the update loop for emitters state --- src/AudObstructionOcclusion.cpp | 65 ++++++++++++++++++++++++++++++++- src/AudObstructionOcclusion.h | 2 + 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 1d483ac..a882290 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -19,7 +19,70 @@ AudObstructionOcclusion::~AudObstructionOcclusion() {} void AudObstructionOcclusion::Update() -{} +{ + + if (g_audioManager == nullptr || g_audioManager->GetState() != AudioState::Enabled) + { + return; + } + + const auto now = std::chrono::steady_clock::now(); + float detlaSeconds = 0.0f; + + if (m_hasUpdated) + { + detlaSeconds = std::chrono::duration(now - m_lastUpdateTime).count(); + } + m_lastUpdateTime = now; + m_hasUpdated = true; + + CcpAutoMutex lock( m_mutex ); + + for (auto it = m_emitters.begin(); it != m_emitters.end();) + { + const AkGameObjectID emitterID = it->first; + EmitterState& state = it->second; + + bool culled = false; + const bool exists = g_audioManager->WithCallbackGameObject(emitterID, [&culled](AudGameObjResource* emitter) { + culled = emitter->IsCulled(); + }); + + if (!exists) + { + it = m_emitters.erase(it); + continue; + } + + const bool obstructionChanged = state.obstruction.Advance(detlaSeconds); + const bool occlusionChanged = state.occlusion.Advance(detlaSeconds); + + if (culled) + { + // The emitter is outside Wwise's view. Keep fading so time stays + // consistent, and make sure the values are resent once the emitter wakes up. + state.needsSend = true; + ++it; + continue; + } + + if (obstructionChanged || occlusionChanged || state.needsSend) + { + state.needsSend = !SendToWwise(emitterID, state); + } + + // Remove the emitter from the list if it has reached its target values and is no longer needed. + if (!state.needsSend && + state.obstruction.ReachedTarget() && state.obstruction.targetValue == 0.0f && + state.occlusion.ReachedTarget() && state.occlusion.targetValue == 0.0f) + { + it = m_emitters.erase(it); + continue; + } + + ++it; + } +} bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion) { diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 41c1394..15161fa 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -58,4 +58,6 @@ class AudObstructionOcclusion bool m_hasUpdated; mutable CcpMutex m_mutex; + std::chrono::steady_clock::time_point m_lastUpdateTime; + }; \ No newline at end of file From 2a167d998bc32c89936b33b4c535ae9ab9bbcbbd Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:17:07 +0000 Subject: [PATCH 07/25] - typo fix - added includes missing - added guards --- src/AudObstructionOcclusion.cpp | 11 +++++------ src/AudObstructionOcclusion.h | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index a882290..9555903 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -10,9 +10,8 @@ AudObstructionOcclusion::AudObstructionOcclusion() : m_fadeRate(0.0f), - m_enabled(false), m_hasUpdated(false), - m_mutex("AudObstructionOcclusion;", "m_mutex") + m_mutex("AudObstructionOcclusion", "m_mutex") {} AudObstructionOcclusion::~AudObstructionOcclusion() @@ -27,11 +26,11 @@ void AudObstructionOcclusion::Update() } const auto now = std::chrono::steady_clock::now(); - float detlaSeconds = 0.0f; + float deltaSeconds = 0.0f; if (m_hasUpdated) { - detlaSeconds = std::chrono::duration(now - m_lastUpdateTime).count(); + deltaSeconds = std::chrono::duration(now - m_lastUpdateTime).count(); } m_lastUpdateTime = now; m_hasUpdated = true; @@ -54,8 +53,8 @@ void AudObstructionOcclusion::Update() continue; } - const bool obstructionChanged = state.obstruction.Advance(detlaSeconds); - const bool occlusionChanged = state.occlusion.Advance(detlaSeconds); + const bool obstructionChanged = state.obstruction.Advance(deltaSeconds); + const bool occlusionChanged = state.occlusion.Advance(deltaSeconds); if (culled) { diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 15161fa..09da032 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -7,7 +7,12 @@ #pragma once -#include +#include +#include + +#include + +#include class AudObstructionOcclusion { @@ -54,7 +59,6 @@ class AudObstructionOcclusion std::unordered_map m_emitters; float m_fadeRate; - bool m_enabled; bool m_hasUpdated; mutable CcpMutex m_mutex; From efb5dc65896855a69323a1693c6a8c48f1d4fb4f Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:20:20 +0000 Subject: [PATCH 08/25] - removeEmitter , reset, clear implementation --- src/AudObstructionOcclusion.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 9555903..53348b9 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -116,13 +116,27 @@ bool AudObstructionOcclusion::SendToWwise(AkGameObjectID emitterID, const Emitte } void AudObstructionOcclusion::RemoveEmitter(AkGameObjectID emitterID) -{} +{ + CcpAutoMutex lock(m_mutex); + m_emitters.erase(emitterID); +} void AudObstructionOcclusion::Reset() -{} +{ + CcpAutoMutex lock(m_mutex); + m_emitters.clear(); + m_hasUpdated = false; +} void AudObstructionOcclusion::ClearAll() -{} +{ + CcpAutoMutex lock(m_mutex); + for (auto& pair : m_emitters) + { + pair.second.obstruction.SetTarget(0.0f, m_fadeRate); + pair.second.occlusion.SetTarget(0.0f, m_fadeRate); + } +} void AudObstructionOcclusion::FadingValue::SetTarget(float target, float fadeRate) { From aa8c083a9f2bad41be3a9d548941c6d3e63faffc Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:36:23 +0000 Subject: [PATCH 09/25] minor change on SetEmitterLineOfSightBlockage --- src/AudObstructionOcclusion.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 53348b9..450db52 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -102,7 +102,17 @@ bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, bool AudObstructionOcclusion::SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage) { - return false; + // When Acoustics is On its transmission already attenuates, so skip occlusion to avoid stacking. + // Might change in the future with the addition of volumes. + const bool acousticsEnabled = g_audioManager != nullptr && g_audioManager->GetSpatialAudioGeometryEnabled(); + + float occlusion = 0.0f; + if (!acousticsEnabled) + { + occlusion = blockage; + } + + return SetObstructionOcclusion(emitterID, 0.0f, occlusion); } bool AudObstructionOcclusion::SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const From 972eabaa202d592fa1c0701d1f31c400b049c633 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:41:00 +0000 Subject: [PATCH 10/25] add guard to avoid setting emutters on the listener --- src/AudObstructionOcclusion.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 450db52..16fb6e0 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -85,6 +85,17 @@ void AudObstructionOcclusion::Update() bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion) { + if (g_audioManager == nullptr || g_audioManager->GetState() != AudioState::Enabled) + { + return false; + } + + // Values are relative to the listener, so setting them on the listener itself is meaningless. + if (emitterID == LISTENER_GAME_OBJ_ID) + { + return false; + } + // We need to ask AudioManager about emitters that actually exist. if (!g_audioManager->WithCallbackGameObject(emitterID, [](AudGameObjResource*) {})) { From 1ad6aba35a291c2c51c504bb5ee7a9156f590556 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:43:46 +0000 Subject: [PATCH 11/25] make fade rate 1 second --- src/AudObstructionOcclusion.cpp | 2 +- src/AudObstructionOcclusion.h | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 16fb6e0..03b69a0 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -9,7 +9,7 @@ #include "AudManager.h" AudObstructionOcclusion::AudObstructionOcclusion() : - m_fadeRate(0.0f), + m_fadeRate(DEFAULT_FADE_RATE), m_hasUpdated(false), m_mutex("AudObstructionOcclusion", "m_mutex") {} diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 09da032..e8cf707 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -57,6 +57,9 @@ class AudObstructionOcclusion bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const; + // Fades from clear to fully blocked in one second. + static constexpr float DEFAULT_FADE_RATE = 1.0f; + std::unordered_map m_emitters; float m_fadeRate; bool m_hasUpdated; From 71b22cafeccfedddbdd0180fcf8c357c89ba3ee8 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:45:47 +0000 Subject: [PATCH 12/25] add getter/setter for enabling occlusion/obstruction --- src/AudObstructionOcclusion.cpp | 25 +++++++++++++++++++++++++ src/AudObstructionOcclusion.h | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 03b69a0..65a17b2 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -11,6 +11,7 @@ AudObstructionOcclusion::AudObstructionOcclusion() : m_fadeRate(DEFAULT_FADE_RATE), m_hasUpdated(false), + m_enabled(true), m_mutex("AudObstructionOcclusion", "m_mutex") {} @@ -85,6 +86,11 @@ void AudObstructionOcclusion::Update() bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion) { + if (!m_enabled) + { + return false; + } + if (g_audioManager == nullptr || g_audioManager->GetState() != AudioState::Enabled) { return false; @@ -159,6 +165,25 @@ void AudObstructionOcclusion::ClearAll() } } +bool AudObstructionOcclusion::GetObstructionOcclusionEnabled() const +{ + return m_enabled; +} + +void AudObstructionOcclusion::SetObstructionOcclusionEnabled(bool value) +{ + if (m_enabled == value) + { + return; + } + + m_enabled = value; + if (!m_enabled) + { + ClearAll(); + } +} + void AudObstructionOcclusion::FadingValue::SetTarget(float target, float fadeRate) { targetValue = std::clamp(target, 0.0f, 1.0f); diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index e8cf707..f8136b3 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -33,6 +33,9 @@ class AudObstructionOcclusion void ClearAll(); + bool GetObstructionOcclusionEnabled() const; + void SetObstructionOcclusionEnabled(bool value); + private: struct FadingValue @@ -63,6 +66,7 @@ class AudObstructionOcclusion std::unordered_map m_emitters; float m_fadeRate; bool m_hasUpdated; + bool m_enabled; mutable CcpMutex m_mutex; std::chrono::steady_clock::time_point m_lastUpdateTime; From b343ee92ac89732fc17985b3dbca4a5e751dc9ec Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:49:45 +0000 Subject: [PATCH 13/25] Get/SetObstructionOcclusionFadeRate added --- src/AudObstructionOcclusion.cpp | 12 ++++++++++++ src/AudObstructionOcclusion.h | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 65a17b2..37bdd80 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -8,6 +8,8 @@ #include "AudObstructionOcclusion.h" #include "AudManager.h" +#include + AudObstructionOcclusion::AudObstructionOcclusion() : m_fadeRate(DEFAULT_FADE_RATE), m_hasUpdated(false), @@ -184,6 +186,16 @@ void AudObstructionOcclusion::SetObstructionOcclusionEnabled(bool value) } } +float AudObstructionOcclusion::GetObstructionOcclusionFadeRate() const +{ + return m_fadeRate; +} + +void AudObstructionOcclusion::SetObstructionOcclusionFadeRate(float value) +{ + m_fadeRate = std::max(value, 0.0f); +} + void AudObstructionOcclusion::FadingValue::SetTarget(float target, float fadeRate) { targetValue = std::clamp(target, 0.0f, 1.0f); diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index f8136b3..81922b4 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -36,6 +36,9 @@ class AudObstructionOcclusion bool GetObstructionOcclusionEnabled() const; void SetObstructionOcclusionEnabled(bool value); + float GetObstructionOcclusionFadeRate() const; + void SetObstructionOcclusionFadeRate(float value); + private: struct FadingValue From d2553547d16d9dab540ce8bc06eb24f244b046cb Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:55:11 +0000 Subject: [PATCH 14/25] expose the audobstructionOcclusion from the audmanager --- src/AudManager.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ src/AudManager.h | 12 ++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/AudManager.cpp b/src/AudManager.cpp index 0d87ee3..72c604c 100644 --- a/src/AudManager.cpp +++ b/src/AudManager.cpp @@ -80,6 +80,7 @@ AudManager::AudManager( IRoot* lockobj ) : // Initialize sound prioritization system m_soundPrioritization = new SoundPrioritization(); m_spatialAudioSettings = new SpatialAudioSettings(); + m_obstructionOcclusion = new AudObstructionOcclusion(); } AudManager::~AudManager() @@ -87,6 +88,7 @@ AudManager::~AudManager() // Clean up sound prioritization system delete m_soundPrioritization; delete m_spatialAudioSettings; + delete m_obstructionOcclusion; if( GetState() == AudioState::Enabled ) { @@ -117,6 +119,8 @@ void AudManager::Process() m_soundPrioritization->CullAudio(); } + m_obstructionOcclusion->Update(); + // Process bank requests, events, positions, RTPC, etc. AK::SoundEngine::RenderAudio(); @@ -340,6 +344,11 @@ void AudManager::UnregisterGameObject( AkGameObjectID gameObjID ) { m_soundPrioritization->UnregisterGameObject( gameObjID ); } + + if( m_obstructionOcclusion ) + { + m_obstructionOcclusion->RemoveEmitter( gameObjID ); + } } bool AudManager::InitCommunication() @@ -530,6 +539,36 @@ const bool AudManager::SpatialAudioIsSupported() return s_systemSupportsSpatialAudio; } +bool AudManager::SetEmitterLineOfSightBlockage( AkGameObjectID emitterID, float blockage ) +{ + return m_obstructionOcclusion->SetEmitterLineOfSightBlockage( emitterID, blockage ); +} + +void AudManager::ClearObstructionOcclusion() +{ + m_obstructionOcclusion->ClearAll(); +} + +bool AudManager::GetObstructionOcclusionEnabled() const +{ + return m_obstructionOcclusion->GetObstructionOcclusionEnabled(); +} + +void AudManager::SetObstructionOcclusionEnabled( bool value ) +{ + m_obstructionOcclusion->SetObstructionOcclusionEnabled( value ); +} + +float AudManager::GetObstructionOcclusionFadeRate() const +{ + return m_obstructionOcclusion->GetObstructionOcclusionFadeRate(); +} + +void AudManager::SetObstructionOcclusionFadeRate( float value ) +{ + m_obstructionOcclusion->SetObstructionOcclusionFadeRate( value ); +} + void AudManager::UpdateSettings( AudSettings* settings ) { m_settings = settings; @@ -776,6 +815,7 @@ void AudManager::Disable() } ClearBanks(); + m_obstructionOcclusion->Reset(); AudGeometry::ClearAllGeometry(); #ifndef AK_OPTIMIZED AK::SoundEngine::UnregisterResourceMonitorCallback(ResourceMonitorCallback); diff --git a/src/AudManager.h b/src/AudManager.h index e8594a3..1c1c4f7 100644 --- a/src/AudManager.h +++ b/src/AudManager.h @@ -9,6 +9,7 @@ #include "AudListener.h" #include "SoundPrioritization.h" #include "SpatialAudioSettings.h" +#include "AudObstructionOcclusion.h" #include "LowLevelIO/LowLevelIOHook.h" #include #include @@ -122,6 +123,16 @@ BLUE_CLASS( AudManager ) : bool GetSpatialAudioGeometryEnabled() const; // Enables or disables spatial audio geometry. void SetSpatialAudioGeometryEnabled( bool enabled ); + // 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 ); + // Fade all obstruction/occlusion values back to clear. + void ClearObstructionOcclusion(); + // Enable or disable game-driven obstruction/occlusion processing. + bool GetObstructionOcclusionEnabled() const; + void SetObstructionOcclusionEnabled( bool value ); + // How fast obstruction/occlusion values fade towards their targets, in units per second. + float GetObstructionOcclusionFadeRate() const; + void SetObstructionOcclusionFadeRate( float value ); // Can be called to see if the current platform supports spatial audio. const bool SpatialAudioIsSupported(); // Stop all currently playing sounds on all game objects. @@ -239,6 +250,7 @@ BLUE_CLASS( AudManager ) : SoundPrioritization* m_soundPrioritization; SpatialAudioSettings* m_spatialAudioSettings; + AudObstructionOcclusion* m_obstructionOcclusion; // Map of game objects, used to guard Wwise callbacks std::unordered_map m_callbackGameObjects; From e3f5196cc42ae3582c5259f8e61b70caa8c5c1da Mon Sep 17 00:00:00 2001 From: phevosccp Date: Tue, 21 Jul 2026 14:57:48 +0000 Subject: [PATCH 15/25] expose audobstruction/occlusion methods to blue --- src/AudManager_Blue.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/AudManager_Blue.cpp b/src/AudManager_Blue.cpp index a4d8c02..9f1bec2 100644 --- a/src/AudManager_Blue.cpp +++ b/src/AudManager_Blue.cpp @@ -49,6 +49,10 @@ const Be::ClassInfo* AudManager::ExposeToBlue() MAP_PROPERTY( "transmissionLoss", GetTransmissionLoss, SetTransmissionLoss, "Per-mesh setting: transmission loss [0.0-1.0] applied to geometry surfaces when meshes are registered.") MAP_PROPERTY( "enableDiffraction", GetEnableDiffraction, SetEnableDiffraction, "Per-mesh setting: enable or disable geometric diffraction on mesh geometry.") MAP_PROPERTY( "enableDiffractionOnBoundaryEdges", GetEnableDiffractionOnBoundaryEdges, SetEnableDiffractionOnBoundaryEdges, "Per-mesh setting: switch to enable or disable geometric diffraction on boundary edges for this mesh.") + + // Obstruction / occlusion + MAP_PROPERTY( "obstructionOcclusionEnabled", GetObstructionOcclusionEnabled, SetObstructionOcclusionEnabled, "Enable or disable game-driven obstruction/occlusion processing. Disabling fades all values back to clear.") + MAP_PROPERTY( "obstructionOcclusionFadeRate", GetObstructionOcclusionFadeRate, SetObstructionOcclusionFadeRate, "How fast obstruction/occlusion values fade towards their targets, in units per second. 0 = instantaneous.") MAP_METHOD_AND_WRAP ( @@ -160,6 +164,18 @@ const Be::ClassInfo* AudManager::ExposeToBlue() ":param stateName: The state you want to set in Wwise." ) MAP_METHOD_AND_WRAP + ( + "SetEmitterLineOfSightBlockage", + SetEmitterLineOfSightBlockage, + "Set a line-of-sight blockage ratio [0.0-1.0] for an emitter (0 = clear). Returns True if the emitter exists." + ) + MAP_METHOD_AND_WRAP + ( + "ClearObstructionOcclusion", + ClearObstructionOcclusion, + "Fade the obstruction/occlusion values of all tracked emitters back to clear." + ) + MAP_METHOD_AND_WRAP ( "SpatialAudioIsSupported", SpatialAudioIsSupported, From 42ac11377032f7873f3df130310a597b32eec2df Mon Sep 17 00:00:00 2001 From: phevosccp Date: Wed, 22 Jul 2026 11:21:50 +0000 Subject: [PATCH 16/25] temporarily revert carbon blue/core version bumps --- vcpkg.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vcpkg.json b/vcpkg.json index cc55d5c..00cb129 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,7 +2,7 @@ "dependencies": [ { "name": "carbon-blue", - "version>=": "6.0.0" + "version>=": "5.1.2" }, { "name": "carbon-math", @@ -27,10 +27,6 @@ "name": "carbon-exefile", "version>=": "4.1.1", "host": true - }, - { - "name": "carbon-core", - "version>=": "3.0.1" } ], "overrides": [ From 69acb3e6aa0f8bd1834056523496edce1da012f4 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Wed, 22 Jul 2026 11:23:12 +0000 Subject: [PATCH 17/25] install to monolith fix --- InstallToMonolith.vcxproj | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/InstallToMonolith.vcxproj b/InstallToMonolith.vcxproj index 763d566..f051a51 100644 --- a/InstallToMonolith.vcxproj +++ b/InstallToMonolith.vcxproj @@ -20,14 +20,10 @@ - $(CCP_EVE_PERFORCE_BRANCH_PATH) $([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Environment', 'CCP_EVE_PERFORCE_BRANCH_PATH')) $(MonolithBranchRoot.Replace('\', '/'))/vendor/github.com/ccpgames/carbon-audio/develop + $(MonolithBranchRoot)\vendor\github.com\ccpgames\carbon-audio\develop .cmake-build-monolith $(ProjectDir.Replace('\', '/')) setlocal @@ -39,6 +35,9 @@ set "PATH_TO_VCPKG_ROOT=$(ProjectDir)vendor\github.com\microsoft\vcpkg" echo Installing CarbonAudio to: $(MonolithInstallPrefix) cmake -S . -B $(MonolithBuildDir) -G "Visual Studio 18 2026" "-DCMAKE_TOOLCHAIN_FILE=$(RepoRoot)vendor/github.com/microsoft/vcpkg/scripts/buildsystems/vcpkg.cmake" "-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$(RepoRoot)vendor/github.com/carbonengine/vcpkg-registry/toolchains/x64-windows-145-carbon.cmake" "-DVCPKG_OVERLAY_TRIPLETS=$(RepoRoot)vendor/github.com/carbonengine/vcpkg-registry/triplets" -DVCPKG_TARGET_TRIPLET=x64-windows-internal -DVCPKG_HOST_TRIPLET=x64-windows-internal -DVCPKG_USE_HOST_TOOLS=ON -DINSTALL_TO_MONOLITH=ON -DBUILD_TESTING=OFF "-DCMAKE_INSTALL_PREFIX=$(MonolithInstallPrefix)" if errorlevel 1 exit /b 1 +rem Wipe any existing install so this build fully replaces it (cmake install never removes stale files). +if exist "$(MonolithInstallDirWin)" echo Removing existing install at: $(MonolithInstallDirWin) +if exist "$(MonolithInstallDirWin)" rmdir /s /q "$(MonolithInstallDirWin)" cmake --build $(MonolithBuildDir) --config Internal --target INSTALL if errorlevel 1 exit /b 1 echo === CarbonAudio installed to $(MonolithInstallPrefix) === @@ -50,7 +49,9 @@ echo === CarbonAudio installed to $(MonolithInstallPrefix) === if exist $(MonolithBuildDir) rmdir /s /q $(MonolithBuildDir) $(MonolithBuildScript) cd /d "$(ProjectDir)" -if exist $(MonolithBuildDir) rmdir /s /q $(MonolithBuildDir) +if exist $(MonolithBuildDir) rmdir /s /q $(MonolithBuildDir) +if exist "$(MonolithInstallDirWin)" echo Removing install at: $(MonolithInstallDirWin) +if exist "$(MonolithInstallDirWin)" rmdir /s /q "$(MonolithInstallDirWin)" $(ProjectDir)$(MonolithBuildDir)\Internal\_audio2_internal.pyd WIN32;NDEBUG;$(NMakePreprocessorDefinitions) From 2834a906c6127a1351f0a1f24641f84dc9539eec Mon Sep 17 00:00:00 2001 From: phevosccp Date: Wed, 22 Jul 2026 14:01:32 +0000 Subject: [PATCH 18/25] flaky test fix --- .../test/test_audgameobj_enabled_exposure.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py index b772a09..7015982 100644 --- a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py +++ b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py @@ -265,20 +265,21 @@ def test_audgameobjresource_wakes_if_muted(self): def test_audgameobjresource_sanitizes_events(self): - def check_playing_id(playingID): + def assert_event_sanitized(event_name) + playingID = self.emitter.SendEvent(event_name) + tries = 0 + while playingID <= 0 and tries < 5: + blue.pyos.synchro.SleepWallclock(100) + blue.os.Pump() + playingID = self.emitter.SendEvent(event_name) + tries += 1 self.assertTrue(playingID > 0) - playingID = self.emitter.SendEvent(" {}".format(ONE_SHOT_EVENT)) - self.wait_for_audio_condition(check_playing_id, playingID) - - playingID = self.emitter.SendEvent("{} ".format(ONE_SHOT_EVENT)) - self.wait_for_audio_condition(check_playing_id, playingID) - - playingID = self.emitter.SendEvent(" {} ".format(ONE_SHOT_EVENT)) - self.wait_for_audio_condition(check_playing_id, playingID) - - playingID = self.emitter.SendEvent(" {}\n \t \r".format(ONE_SHOT_EVENT)) - self.wait_for_audio_condition(check_playing_id, playingID) + assert_event_sanitized(" {}".format(ONE_SHOT_EVENT)) + assert_event_sanitized("{} ".format(ONE_SHOT_EVENT)) + assert_event_sanitized(" {} ".format(ONE_SHOT_EVENT)) + assert_event_sanitized(" {}\n \t \r".format(ONE_SHOT_EVENT)) + assert_event_sanitized("\n\n\n\n\n\n{}\n\n\n\n\n".format(ONE_SHOT_EVENT)) playingID = self.emitter.SendEvent("\n\n\n\n\n\n{}\n\n\n\n\n".format(ONE_SHOT_EVENT)) self.wait_for_audio_condition(check_playing_id, playingID) @@ -286,5 +287,4 @@ def check_playing_id(playingID): # Test with event prefixes prefix = ONE_SHOT_EVENT[:5] self.emitter.eventPrefix = prefix - playingID = self.emitter.SendEvent(" {}\n".format(ONE_SHOT_EVENT[5:])) - self.wait_for_audio_condition(check_playing_id, playingID) \ No newline at end of file + assert_event_sanitized(" {}\n".format(ONE_SHOT_EVENT[5:])) \ No newline at end of file From 1d01b525f5983a73d7d3a59cc8edb05f12552447 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Wed, 22 Jul 2026 14:02:12 +0000 Subject: [PATCH 19/25] flaky test incomplete commit --- .../audiotests/test/test_audgameobj_enabled_exposure.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py index 7015982..a98f63c 100644 --- a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py +++ b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py @@ -32,7 +32,7 @@ def setUp(self): self.listener = audio2.GetListener() self.listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) - + def tearDown(self): self.emitter.eventPrefix = "" self.emitter.StopAll() @@ -281,10 +281,7 @@ def assert_event_sanitized(event_name) assert_event_sanitized(" {}\n \t \r".format(ONE_SHOT_EVENT)) assert_event_sanitized("\n\n\n\n\n\n{}\n\n\n\n\n".format(ONE_SHOT_EVENT)) - playingID = self.emitter.SendEvent("\n\n\n\n\n\n{}\n\n\n\n\n".format(ONE_SHOT_EVENT)) - self.wait_for_audio_condition(check_playing_id, playingID) - # Test with event prefixes prefix = ONE_SHOT_EVENT[:5] - self.emitter.eventPrefix = prefix + self.emitter.eventPrefix = prefix assert_event_sanitized(" {}\n".format(ONE_SHOT_EVENT[5:])) \ No newline at end of file From f43493c6d96511204936b88141fa14440734902f Mon Sep 17 00:00:00 2001 From: phevosccp Date: Wed, 22 Jul 2026 14:26:56 +0000 Subject: [PATCH 20/25] minor typo --- .../python/audiotests/test/test_audgameobj_enabled_exposure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py index a98f63c..a19d35d 100644 --- a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py +++ b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py @@ -265,7 +265,7 @@ def test_audgameobjresource_wakes_if_muted(self): def test_audgameobjresource_sanitizes_events(self): - def assert_event_sanitized(event_name) + def assert_event_sanitized(event_name): playingID = self.emitter.SendEvent(event_name) tries = 0 while playingID <= 0 and tries < 5: From 8c9365b591d6e2fffe6db485a3601a18b2ba1441 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Fri, 24 Jul 2026 09:29:50 +0000 Subject: [PATCH 21/25] requested PR changes --- InstallToMonolith.vcxproj | 3 +-- src/AudObstructionOcclusion.cpp | 2 +- src/AudObstructionOcclusion.h | 2 +- vcpkg.json | 6 +++++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/InstallToMonolith.vcxproj b/InstallToMonolith.vcxproj index f051a51..931fdf8 100644 --- a/InstallToMonolith.vcxproj +++ b/InstallToMonolith.vcxproj @@ -23,7 +23,7 @@ $(CCP_EVE_PERFORCE_BRANCH_PATH) $([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Environment', 'CCP_EVE_PERFORCE_BRANCH_PATH')) $(MonolithBranchRoot.Replace('\', '/'))/vendor/github.com/ccpgames/carbon-audio/develop - $(MonolithBranchRoot)\vendor\github.com\ccpgames\carbon-audio\develop + $(MonolithBranchRoot)\vendor\github.com\carbonengine\audio\develop .cmake-build-monolith $(ProjectDir.Replace('\', '/')) setlocal @@ -35,7 +35,6 @@ set "PATH_TO_VCPKG_ROOT=$(ProjectDir)vendor\github.com\microsoft\vcpkg" echo Installing CarbonAudio to: $(MonolithInstallPrefix) cmake -S . -B $(MonolithBuildDir) -G "Visual Studio 18 2026" "-DCMAKE_TOOLCHAIN_FILE=$(RepoRoot)vendor/github.com/microsoft/vcpkg/scripts/buildsystems/vcpkg.cmake" "-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$(RepoRoot)vendor/github.com/carbonengine/vcpkg-registry/toolchains/x64-windows-145-carbon.cmake" "-DVCPKG_OVERLAY_TRIPLETS=$(RepoRoot)vendor/github.com/carbonengine/vcpkg-registry/triplets" -DVCPKG_TARGET_TRIPLET=x64-windows-internal -DVCPKG_HOST_TRIPLET=x64-windows-internal -DVCPKG_USE_HOST_TOOLS=ON -DINSTALL_TO_MONOLITH=ON -DBUILD_TESTING=OFF "-DCMAKE_INSTALL_PREFIX=$(MonolithInstallPrefix)" if errorlevel 1 exit /b 1 -rem Wipe any existing install so this build fully replaces it (cmake install never removes stale files). if exist "$(MonolithInstallDirWin)" echo Removing existing install at: $(MonolithInstallDirWin) if exist "$(MonolithInstallDirWin)" rmdir /s /q "$(MonolithInstallDirWin)" cmake --build $(MonolithBuildDir) --config Internal --target INSTALL diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 37bdd80..5e1adb2 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -2,7 +2,7 @@ // // Creator: Phevos Rinis // Creation Date: Jul 2026 -// Copyright (c) 2026 CCP Games +// Copyright (c) 2026 Fernis Creations // #include "AudObstructionOcclusion.h" diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 81922b4..4cc9aaf 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -2,7 +2,7 @@ // // Creator: Phevos Rinis // Creation Date: Jul 2026 -// Copyright (c) 2026 CCP Games +// Copyright (c) 2026 Fernis Creations // #pragma once diff --git a/vcpkg.json b/vcpkg.json index 00cb129..cc55d5c 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,7 +2,7 @@ "dependencies": [ { "name": "carbon-blue", - "version>=": "5.1.2" + "version>=": "6.0.0" }, { "name": "carbon-math", @@ -27,6 +27,10 @@ "name": "carbon-exefile", "version>=": "4.1.1", "host": true + }, + { + "name": "carbon-core", + "version>=": "3.0.1" } ], "overrides": [ From 6a6e6210bd7006e1e6e9b7428e85b5b62bd5c384 Mon Sep 17 00:00:00 2001 From: phevosccp Date: Fri, 24 Jul 2026 11:28:35 +0000 Subject: [PATCH 22/25] requested PR changes #2 --- src/AudManager.cpp | 3 +- src/AudManager.h | 2 +- src/AudObstructionOcclusion.cpp | 46 ++++++++----------- src/AudObstructionOcclusion.h | 10 ++-- .../test/test_audgameobj_enabled_exposure.py | 35 +++++++------- 5 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/AudManager.cpp b/src/AudManager.cpp index 72c604c..53d0a5b 100644 --- a/src/AudManager.cpp +++ b/src/AudManager.cpp @@ -80,7 +80,7 @@ AudManager::AudManager( IRoot* lockobj ) : // Initialize sound prioritization system m_soundPrioritization = new SoundPrioritization(); m_spatialAudioSettings = new SpatialAudioSettings(); - m_obstructionOcclusion = new AudObstructionOcclusion(); + m_obstructionOcclusion = std::make_unique( this ); } AudManager::~AudManager() @@ -88,7 +88,6 @@ AudManager::~AudManager() // Clean up sound prioritization system delete m_soundPrioritization; delete m_spatialAudioSettings; - delete m_obstructionOcclusion; if( GetState() == AudioState::Enabled ) { diff --git a/src/AudManager.h b/src/AudManager.h index 1c1c4f7..96b0702 100644 --- a/src/AudManager.h +++ b/src/AudManager.h @@ -250,7 +250,7 @@ BLUE_CLASS( AudManager ) : SoundPrioritization* m_soundPrioritization; SpatialAudioSettings* m_spatialAudioSettings; - AudObstructionOcclusion* m_obstructionOcclusion; + std::unique_ptr m_obstructionOcclusion; // Map of game objects, used to guard Wwise callbacks std::unordered_map m_callbackGameObjects; diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 5e1adb2..a16eabf 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -10,7 +10,8 @@ #include -AudObstructionOcclusion::AudObstructionOcclusion() : +AudObstructionOcclusion::AudObstructionOcclusion(AudManager* audioManager) : + m_audioManager(audioManager), m_fadeRate(DEFAULT_FADE_RATE), m_hasUpdated(false), m_enabled(true), @@ -23,7 +24,7 @@ AudObstructionOcclusion::~AudObstructionOcclusion() void AudObstructionOcclusion::Update() { - if (g_audioManager == nullptr || g_audioManager->GetState() != AudioState::Enabled) + if (m_audioManager == nullptr || m_audioManager->GetState() != AudioState::Enabled) { return; } @@ -46,7 +47,7 @@ void AudObstructionOcclusion::Update() EmitterState& state = it->second; bool culled = false; - const bool exists = g_audioManager->WithCallbackGameObject(emitterID, [&culled](AudGameObjResource* emitter) { + const bool exists = m_audioManager->WithCallbackGameObject(emitterID, [&culled](AudGameObjResource* emitter) { culled = emitter->IsCulled(); }); @@ -56,8 +57,8 @@ void AudObstructionOcclusion::Update() continue; } - const bool obstructionChanged = state.obstruction.Advance(deltaSeconds); - const bool occlusionChanged = state.occlusion.Advance(deltaSeconds); + const bool obstructionChanged = state.obstruction.Advance(deltaSeconds, m_fadeRate); + const bool occlusionChanged = state.occlusion.Advance(deltaSeconds, m_fadeRate); if (culled) { @@ -93,7 +94,7 @@ bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, return false; } - if (g_audioManager == nullptr || g_audioManager->GetState() != AudioState::Enabled) + if (m_audioManager == nullptr || m_audioManager->GetState() != AudioState::Enabled) { return false; } @@ -105,7 +106,7 @@ bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, } // We need to ask AudioManager about emitters that actually exist. - if (!g_audioManager->WithCallbackGameObject(emitterID, [](AudGameObjResource*) {})) + if (!m_audioManager->WithCallbackGameObject(emitterID, [](AudGameObjResource*) {})) { return false; } @@ -113,8 +114,8 @@ bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, CcpAutoMutex lock(m_mutex); EmitterState& state = m_emitters[emitterID]; - state.obstruction.SetTarget(obstruction, m_fadeRate); - state.occlusion.SetTarget(occlusion, m_fadeRate); + state.obstruction.SetTarget(obstruction); + state.occlusion.SetTarget(occlusion); return true; } @@ -123,7 +124,7 @@ bool AudObstructionOcclusion::SetEmitterLineOfSightBlockage(AkGameObjectID emitt { // When Acoustics is On its transmission already attenuates, so skip occlusion to avoid stacking. // Might change in the future with the addition of volumes. - const bool acousticsEnabled = g_audioManager != nullptr && g_audioManager->GetSpatialAudioGeometryEnabled(); + const bool acousticsEnabled = m_audioManager != nullptr && m_audioManager->GetSpatialAudioGeometryEnabled(); float occlusion = 0.0f; if (!acousticsEnabled) @@ -162,8 +163,8 @@ void AudObstructionOcclusion::ClearAll() CcpAutoMutex lock(m_mutex); for (auto& pair : m_emitters) { - pair.second.obstruction.SetTarget(0.0f, m_fadeRate); - pair.second.occlusion.SetTarget(0.0f, m_fadeRate); + pair.second.obstruction.SetTarget(0.0f); + pair.second.occlusion.SetTarget(0.0f); } } @@ -196,43 +197,34 @@ void AudObstructionOcclusion::SetObstructionOcclusionFadeRate(float value) m_fadeRate = std::max(value, 0.0f); } -void AudObstructionOcclusion::FadingValue::SetTarget(float target, float fadeRate) +void AudObstructionOcclusion::FadingValue::SetTarget(float target) { targetValue = std::clamp(target, 0.0f, 1.0f); - - if (targetValue >= currentValue) - { - rate = fadeRate; - } - else - { - rate = -fadeRate; - } } -bool AudObstructionOcclusion::FadingValue::Advance(float deltaSeconds) +bool AudObstructionOcclusion::FadingValue::Advance(float deltaSeconds, float fadeRate) { if (currentValue == targetValue) { return false; } - if (rate == 0.0f) + if (fadeRate <= 0.0f) { currentValue = targetValue; return true; } const float oldValue = currentValue; - const float newValue = oldValue + rate * deltaSeconds; + const float step = fadeRate * deltaSeconds; if (oldValue > targetValue) { - currentValue = std::clamp(newValue, targetValue, oldValue); + currentValue = std::clamp(oldValue - step, targetValue, oldValue); } else { - currentValue = std::clamp(newValue, oldValue, targetValue); + currentValue = std::clamp(oldValue + step, oldValue, targetValue); } return currentValue != oldValue; } diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 4cc9aaf..adaa756 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -14,11 +14,13 @@ #include +class AudManager; + class AudObstructionOcclusion { public: - AudObstructionOcclusion(); + AudObstructionOcclusion(AudManager* audioManager); ~AudObstructionOcclusion(); void Update(); @@ -45,10 +47,9 @@ class AudObstructionOcclusion { float currentValue = 0.0f; float targetValue = 0.0f; - float rate = 0.0f; - void SetTarget(float target, float fadeRate); - bool Advance(float deltaSeconds); + void SetTarget(float target); + bool Advance(float deltaSeconds, float fadeRate); bool ReachedTarget() const { return currentValue == targetValue; }; }; @@ -66,6 +67,7 @@ class AudObstructionOcclusion // Fades from clear to fully blocked in one second. static constexpr float DEFAULT_FADE_RATE = 1.0f; + AudManager* m_audioManager; std::unordered_map m_emitters; float m_fadeRate; bool m_hasUpdated; diff --git a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py index a19d35d..b772a09 100644 --- a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py +++ b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py @@ -32,7 +32,7 @@ def setUp(self): self.listener = audio2.GetListener() self.listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) - + def tearDown(self): self.emitter.eventPrefix = "" self.emitter.StopAll() @@ -265,23 +265,26 @@ def test_audgameobjresource_wakes_if_muted(self): def test_audgameobjresource_sanitizes_events(self): - def assert_event_sanitized(event_name): - playingID = self.emitter.SendEvent(event_name) - tries = 0 - while playingID <= 0 and tries < 5: - blue.pyos.synchro.SleepWallclock(100) - blue.os.Pump() - playingID = self.emitter.SendEvent(event_name) - tries += 1 + def check_playing_id(playingID): self.assertTrue(playingID > 0) - assert_event_sanitized(" {}".format(ONE_SHOT_EVENT)) - assert_event_sanitized("{} ".format(ONE_SHOT_EVENT)) - assert_event_sanitized(" {} ".format(ONE_SHOT_EVENT)) - assert_event_sanitized(" {}\n \t \r".format(ONE_SHOT_EVENT)) - assert_event_sanitized("\n\n\n\n\n\n{}\n\n\n\n\n".format(ONE_SHOT_EVENT)) + playingID = self.emitter.SendEvent(" {}".format(ONE_SHOT_EVENT)) + self.wait_for_audio_condition(check_playing_id, playingID) + + playingID = self.emitter.SendEvent("{} ".format(ONE_SHOT_EVENT)) + self.wait_for_audio_condition(check_playing_id, playingID) + + playingID = self.emitter.SendEvent(" {} ".format(ONE_SHOT_EVENT)) + self.wait_for_audio_condition(check_playing_id, playingID) + + playingID = self.emitter.SendEvent(" {}\n \t \r".format(ONE_SHOT_EVENT)) + self.wait_for_audio_condition(check_playing_id, playingID) + + playingID = self.emitter.SendEvent("\n\n\n\n\n\n{}\n\n\n\n\n".format(ONE_SHOT_EVENT)) + self.wait_for_audio_condition(check_playing_id, playingID) # Test with event prefixes prefix = ONE_SHOT_EVENT[:5] - self.emitter.eventPrefix = prefix - assert_event_sanitized(" {}\n".format(ONE_SHOT_EVENT[5:])) \ No newline at end of file + self.emitter.eventPrefix = prefix + playingID = self.emitter.SendEvent(" {}\n".format(ONE_SHOT_EVENT[5:])) + self.wait_for_audio_condition(check_playing_id, playingID) \ No newline at end of file From a44d88e153989037f99bbeb7dd024f988c977a5b Mon Sep 17 00:00:00 2001 From: phevosccp Date: Mon, 27 Jul 2026 10:30:34 +0000 Subject: [PATCH 23/25] another attempt to fix flaky test --- .../test/test_audgameobj_enabled_exposure.py | 21 +++++++++---------- .../test/test_audmanager_exposure.py | 6 +++--- .../test/test_auduiplayer_exposure.py | 11 ++++++++-- .../test/test_python_audiomanager.py | 14 +++++++++---- tests/python/audiotests/utils.py | 18 ++++++++++++++++ 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py index 0d6c9e1..04611b6 100644 --- a/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py +++ b/tests/python/audiotests/test/test_audgameobj_enabled_exposure.py @@ -8,7 +8,7 @@ NONESSENTIAL_BNK, NONESSENTIAL_BNK_EVENT, NONESSENTIAL_STREAM_BNK, NONESSENTIAL_STREAM_EVENT ) from audiotests.base_test_class import BaseAudio2TestClass -from audiotests.utils import PumpOSWithTimeout +from audiotests.utils import WaitForEmitterToWake, WaitForSoundBanksToLoad @@ -31,8 +31,14 @@ def setUp(self): self.listener = audio2.GetListener() self.listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) - + self.assertTrue( + WaitForSoundBanksToLoad([ + LOOP_EVENT, ONE_SHOT_EVENT, ESSENTIAL_EVENT, NONESSENTIAL_BNK_EVENT, NONESSENTIAL_STREAM_EVENT + ]), + "Timed out waiting for the test SoundBanks to load." + ) + self.assertTrue(WaitForEmitterToWake(self.emitter), "Timed out waiting for the emitter to be woken up.") + def tearDown(self): self.emitter.eventPrefix = "" self.emitter.StopAll() @@ -266,14 +272,7 @@ def test_audgameobjresource_wakes_if_muted(self): def test_audgameobjresource_sanitizes_events(self): def assert_event_sanitized(event_name): - playingID = self.emitter.SendEvent(event_name) - tries = 0 - while playingID <= 0 and tries < 5: - blue.pyos.synchro.SleepWallclock(100) - blue.os.Pump() - playingID = self.emitter.SendEvent(event_name) - tries += 1 - self.assertTrue(playingID > 0) + self.assertTrue(self.emitter.SendEvent(event_name) > 0) assert_event_sanitized(" {}".format(ONE_SHOT_EVENT)) assert_event_sanitized("{} ".format(ONE_SHOT_EVENT)) diff --git a/tests/python/audiotests/test/test_audmanager_exposure.py b/tests/python/audiotests/test/test_audmanager_exposure.py index 198482e..104ba94 100644 --- a/tests/python/audiotests/test/test_audmanager_exposure.py +++ b/tests/python/audiotests/test/test_audmanager_exposure.py @@ -8,7 +8,7 @@ from audiotests.base_test_class import COMMON_BNK, LOOP_BNK, LOOP_EVENT, ONE_SHOT_BNK from audiotests.base_test_class import GetAudioMetadataFromFile -from audiotests.utils import PumpOSWithTimeout +from audiotests.utils import PumpOSWithTimeout, WaitForSoundBanksToLoad class TestAudManagerExposure(unittest.TestCase): @classmethod @@ -143,11 +143,11 @@ def test_audmanager_stopall(self): import audio2 self.audioManager.LoadBank(COMMON_BNK) self.audioManager.LoadBank(LOOP_BNK) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + self.assertTrue(WaitForSoundBanksToLoad([LOOP_EVENT]), "Timed out waiting for the test SoundBanks to load.") emitter1 = audio2.AudEmitter("emitter1") emitter2 = audio2.AudEmitter("emitter2") - self.assertTrue(emitter1.SendEvent(LOOP_EVENT) > 0) + self.assertTrue(emitter1.SendEvent(LOOP_EVENT) > 0) self.assertTrue(emitter2.SendEvent(LOOP_EVENT) > 0) self.audioManager.StopAll() diff --git a/tests/python/audiotests/test/test_auduiplayer_exposure.py b/tests/python/audiotests/test/test_auduiplayer_exposure.py index 40688a9..8a3cce4 100644 --- a/tests/python/audiotests/test/test_auduiplayer_exposure.py +++ b/tests/python/audiotests/test/test_auduiplayer_exposure.py @@ -4,7 +4,7 @@ from audiotests.base_test_class import COMMON_BNK, LOOP_BNK, LOOP_EVENT, ONE_SHOT_BNK, ONE_SHOT_EVENT from audiotests.base_test_class import BaseAudio2TestClass -from audiotests.utils import PumpOSWithTimeout +from audiotests.utils import PumpOSWithTimeout, WaitForEmitterToWake, WaitForSoundBanksToLoad class TestAudUIPlayerExposure(BaseAudio2TestClass): @@ -14,8 +14,15 @@ def setUpClass(cls): cls.Initialize(cls, defaultSoundBanks=[COMMON_BNK, LOOP_BNK, ONE_SHOT_BNK]) def setUp(self): + import audio2 self.audioManager.Enable() - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + uiPlayer = audio2.GetUIPlayer() + uiPlayer.SetPlacement((0,0,0), (0,0,0), (0,0,0)) + self.assertTrue( + WaitForSoundBanksToLoad([LOOP_EVENT, ONE_SHOT_EVENT]), + "Timed out waiting for the test SoundBanks to load." + ) + self.assertTrue(WaitForEmitterToWake(uiPlayer), "Timed out waiting for the UI player to be woken up.") def tearDown(self): self.audioManager.Disable() diff --git a/tests/python/audiotests/test/test_python_audiomanager.py b/tests/python/audiotests/test/test_python_audiomanager.py index 48b197a..5a332e1 100644 --- a/tests/python/audiotests/test/test_python_audiomanager.py +++ b/tests/python/audiotests/test/test_python_audiomanager.py @@ -4,7 +4,7 @@ from audiotests.base_test_class import COMMON_BNK, LOOP_BNK, LOOP_EVENT, ONE_SHOT_BNK, ONE_SHOT_EVENT, SOUNDBANK_FILEPATH from audiotests.base_test_class import BaseAudio2TestClass -from audiotests.utils import GetAudioMetadataFromFile, PumpOSWithTimeout +from audiotests.utils import GetAudioMetadataFromFile, PumpOSWithTimeout, WaitForEmitterToWake, WaitForSoundBanksToLoad class TestPythonAudioManager(BaseAudio2TestClass): @classmethod @@ -68,7 +68,8 @@ def test_disable_then_enable_can_load_bank_and_play_event(self): emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) listener = audio2.GetListener() listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + self.assertTrue(WaitForSoundBanksToLoad([LOOP_EVENT]), "Timed out waiting for the test SoundBanks to load.") + self.assertTrue(WaitForEmitterToWake(emitter), "Timed out waiting for the emitter to be woken up.") playingID = emitter.SendEvent(LOOP_EVENT) self.assertTrue(playingID > 0) emitter.StopAll() @@ -83,7 +84,8 @@ def test_stopped_loop_does_not_resume_after_disable_enable(self): emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) listener = audio2.GetListener() listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + self.assertTrue(WaitForSoundBanksToLoad([LOOP_EVENT]), "Timed out waiting for the test SoundBanks to load.") + self.assertTrue(WaitForEmitterToWake(emitter), "Timed out waiting for the emitter to be woken up.") playingID = emitter.SendEvent(LOOP_EVENT) self.assertTrue(playingID > 0) @@ -110,7 +112,11 @@ def test_loop_stopped_by_posted_event_does_not_resume_after_disable_enable(self) emitter.SetPlacement((0,0,0), (0,0,0), (0,0,0)) listener = audio2.GetListener() listener.SetPosition((0,0,0), (0,0,0), (0,0,0)) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + self.assertTrue( + WaitForSoundBanksToLoad([LOOP_EVENT, ONE_SHOT_EVENT]), + "Timed out waiting for the test SoundBanks to load." + ) + self.assertTrue(WaitForEmitterToWake(emitter), "Timed out waiting for the emitter to be woken up.") loopPlayingID = emitter.SendEvent(LOOP_EVENT) self.assertTrue(loopPlayingID > 0) diff --git a/tests/python/audiotests/utils.py b/tests/python/audiotests/utils.py index 87296b0..ebf3f38 100644 --- a/tests/python/audiotests/utils.py +++ b/tests/python/audiotests/utils.py @@ -15,6 +15,24 @@ def PumpOSWithTimeout(booleanFunc, maxTries=10): numTries += 1 return not booleanFunc() +def WaitForSoundBanksToLoad(events, maxTries=20): + """Pump until every given event can be posted, which means its SoundBank has finished loading. + SoundBanks are loaded asynchronously so SendEvent returns 0 until the load callback has been hit. + """ + import audio2 + probe = audio2.AudEmitter("soundBankLoadProbe") + probe.SetPlacement((0,0,0), (0,0,0), (0,0,0)) + allLoaded = True + for event in events: + if not PumpOSWithTimeout(lambda event=event: probe.SendEvent(event) <= 0, maxTries=maxTries): + allLoaded = False + probe.StopAll() + return allLoaded + +def WaitForEmitterToWake(emitter, maxTries=10): + """Pump until sound prioritization has woken the emitter. Culled emitters queue events instead of posting them.""" + return PumpOSWithTimeout(emitter.IsCulled, maxTries=maxTries) + def GetAudioMetadataFromFile(): """Gets audio metadata from file and returns it as a dict. Also converts eventIDs to int in the process.""" with open(AUDIO_METADATA_FILEPATH, "r") as f: From 16fb516ad4edf0e778546a1afee45147d9e6a83d Mon Sep 17 00:00:00 2001 From: phevosccp Date: Mon, 27 Jul 2026 16:49:02 +0000 Subject: [PATCH 24/25] - Added doxygen - Added test coverage - Rest of the requested PR changes. --- src/AudManager.cpp | 13 +- src/AudManager.h | 2 + src/AudManager_Blue.cpp | 8 + src/AudObstructionOcclusion.cpp | 21 ++- src/AudObstructionOcclusion.h | 54 +++++- .../test_obstruction_occlusion_exposure.py | 176 ++++++++++++++++++ 6 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 tests/python/audiotests/test/test_obstruction_occlusion_exposure.py diff --git a/src/AudManager.cpp b/src/AudManager.cpp index 53d0a5b..5eecdef 100644 --- a/src/AudManager.cpp +++ b/src/AudManager.cpp @@ -543,6 +543,11 @@ bool AudManager::SetEmitterLineOfSightBlockage( AkGameObjectID emitterID, float return m_obstructionOcclusion->SetEmitterLineOfSightBlockage( emitterID, blockage ); } +float AudManager::GetEmitterOcclusion( AkGameObjectID emitterID ) const +{ + return m_obstructionOcclusion->GetEmitterOcclusion( emitterID ); +} + void AudManager::ClearObstructionOcclusion() { m_obstructionOcclusion->ClearAll(); @@ -550,22 +555,22 @@ void AudManager::ClearObstructionOcclusion() bool AudManager::GetObstructionOcclusionEnabled() const { - return m_obstructionOcclusion->GetObstructionOcclusionEnabled(); + return m_obstructionOcclusion->IsEnabled(); } void AudManager::SetObstructionOcclusionEnabled( bool value ) { - m_obstructionOcclusion->SetObstructionOcclusionEnabled( value ); + m_obstructionOcclusion->SetEnabled( value ); } float AudManager::GetObstructionOcclusionFadeRate() const { - return m_obstructionOcclusion->GetObstructionOcclusionFadeRate(); + return m_obstructionOcclusion->GetFadeRate(); } void AudManager::SetObstructionOcclusionFadeRate( float value ) { - m_obstructionOcclusion->SetObstructionOcclusionFadeRate( value ); + m_obstructionOcclusion->SetFadeRate( value ); } void AudManager::UpdateSettings( AudSettings* settings ) diff --git a/src/AudManager.h b/src/AudManager.h index 96b0702..027a123 100644 --- a/src/AudManager.h +++ b/src/AudManager.h @@ -125,6 +125,8 @@ BLUE_CLASS( AudManager ) : void SetSpatialAudioGeometryEnabled( bool enabled ); // 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. + float GetEmitterOcclusion( AkGameObjectID emitterID ) const; // Fade all obstruction/occlusion values back to clear. void ClearObstructionOcclusion(); // Enable or disable game-driven obstruction/occlusion processing. diff --git a/src/AudManager_Blue.cpp b/src/AudManager_Blue.cpp index 9f1bec2..e032abc 100644 --- a/src/AudManager_Blue.cpp +++ b/src/AudManager_Blue.cpp @@ -170,6 +170,14 @@ const Be::ClassInfo* AudManager::ExposeToBlue() "Set a line-of-sight blockage ratio [0.0-1.0] for an emitter (0 = clear). Returns True if the emitter exists." ) MAP_METHOD_AND_WRAP + ( + "GetEmitterOcclusion", + GetEmitterOcclusion, + "Get the occlusion value currently applied to an emitter. This is the live, mid-fade value " + "rather than the target that was set, so it can be used to observe a fade in progress. " + "Returns 0.0 if the emitter is clear or is not being tracked." + ) + MAP_METHOD_AND_WRAP ( "ClearObstructionOcclusion", ClearObstructionOcclusion, diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index a16eabf..4985409 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -135,6 +135,19 @@ bool AudObstructionOcclusion::SetEmitterLineOfSightBlockage(AkGameObjectID emitt return SetObstructionOcclusion(emitterID, 0.0f, occlusion); } +float AudObstructionOcclusion::GetEmitterOcclusion(AkGameObjectID emitterID) const +{ + CcpAutoMutex lock(m_mutex); + + auto it = m_emitters.find(emitterID); + if (it == m_emitters.end()) + { + return 0.0f; + } + + return it->second.occlusion.currentValue; +} + bool AudObstructionOcclusion::SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const { const AKRESULT result = AK::SoundEngine::SetObjectObstructionAndOcclusion( @@ -168,12 +181,12 @@ void AudObstructionOcclusion::ClearAll() } } -bool AudObstructionOcclusion::GetObstructionOcclusionEnabled() const +bool AudObstructionOcclusion::IsEnabled() const { return m_enabled; } -void AudObstructionOcclusion::SetObstructionOcclusionEnabled(bool value) +void AudObstructionOcclusion::SetEnabled(bool value) { if (m_enabled == value) { @@ -187,12 +200,12 @@ void AudObstructionOcclusion::SetObstructionOcclusionEnabled(bool value) } } -float AudObstructionOcclusion::GetObstructionOcclusionFadeRate() const +float AudObstructionOcclusion::GetFadeRate() const { return m_fadeRate; } -void AudObstructionOcclusion::SetObstructionOcclusionFadeRate(float value) +void AudObstructionOcclusion::SetFadeRate(float value) { m_fadeRate = std::max(value, 0.0f); } diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index adaa756..5f413d7 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -16,6 +16,15 @@ class AudManager; +/** + * @brief Owns the obstruction and occlusion of every emitter and feeds those values to Wwise. + * + * The game tells us how much of an emitter's line of sight to the listener is blocked, and this + * class turns that into the obstruction and occlusion Wwise applies + * (see @c AK::SoundEngine::SetObjectObstructionAndOcclusion). Each + * value fades towards its target so that sounds do not pop when something moves in front of them. + * + */ class AudObstructionOcclusion { public: @@ -23,26 +32,61 @@ class AudObstructionOcclusion AudObstructionOcclusion(AudManager* audioManager); ~AudObstructionOcclusion(); + /** + * @brief Advances every fade and sends the values that changed to Wwise. + */ void Update(); + /** + * @brief Sets the obstruction and occlusion an emitter fades towards. + * + * @param emitterID The emitter to block. + * @param obstruction Target obstruction [0.0, 1.0] + * @param occlusion Target occlusion [0.0, 1.0] + * + * @return True if the emitter exists and the values were accepted. + */ bool SetObstructionOcclusion(AkGameObjectID emitterID, float obstruction, float occlusion); + /** + * @brief Sets how much of an emitter's line of sight to the listener is blocked. + * + * @param emitterID The emitter to block. + * @param blockage How blocked the line of sight is [0.0, 1.0]. 0 is a clear line of sight. + * + * @return True if the emitter exists and the value was accepted. + */ bool SetEmitterLineOfSightBlockage(AkGameObjectID emitterID, float blockage); + /** + * @brief The occlusion currently applied to an emitter. + */ + float GetEmitterOcclusion(AkGameObjectID emitterID) const; + + /// Drops an emitter straight away without fading it out, for when the game object goes away. void RemoveEmitter( AkGameObjectID emitterID ); + /// Forgets every emitter and the fade clock, for when audio is disabled. void Reset(); + /// Fades every tracked emitter back to clear and keeps them around until they get there. void ClearAll(); - bool GetObstructionOcclusionEnabled() const; - void SetObstructionOcclusionEnabled(bool value); + /** + * @brief Controls whether game driven obstruction and occlusion is processed at all. + */ + bool IsEnabled() const; + void SetEnabled(bool value); - float GetObstructionOcclusionFadeRate() const; - void SetObstructionOcclusionFadeRate(float value); + /** + * @brief How fast values move towards their target, in units per second. + */ + float GetFadeRate() const; + void SetFadeRate(float value); private: + /// A single value on its way to a target, and the maths that moves it there. struct FadingValue { float currentValue = 0.0f; @@ -53,6 +97,7 @@ class AudObstructionOcclusion bool ReachedTarget() const { return currentValue == targetValue; }; }; + /// Everything we keep for one blocked emitter. struct EmitterState { FadingValue obstruction; @@ -64,7 +109,6 @@ class AudObstructionOcclusion bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const; - // Fades from clear to fully blocked in one second. static constexpr float DEFAULT_FADE_RATE = 1.0f; AudManager* m_audioManager; diff --git a/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py b/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py new file mode 100644 index 0000000..b0aeb9b --- /dev/null +++ b/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py @@ -0,0 +1,176 @@ +# Copyright © 2026 CCP ehf. + +from audiotests.base_test_class import COMMON_BNK +from audiotests.base_test_class import BaseAudio2TestClass +from audiotests.utils import PumpOSWithTimeout + + +INSTANT_FADE_RATE = 0.0 +SLOW_FADE_RATE = 0.01 +FAST_FADE_RATE = 100.0 + + +class TestObstructionOcclusionExposure(BaseAudio2TestClass): + """Tests line-of-sight occlusion, in particular the interpolation that produces the fade.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Initialize(cls, defaultSoundBanks=[COMMON_BNK]) + + def setUp(self): + import audio2 + self.audioManager.Enable() + self.manager = self.audioManager.manager + + self.emitter = audio2.AudEmitter("occlusionTestEmitter") + self.emitter.SetPlacement((0, 0, 0), (0, 0, 0), (0, 0, 0)) + + self.manager.obstructionOcclusionEnabled = True + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + + def tearDown(self): + self.emitter = None + self.audioManager.Disable() + + def Pump(self, times=3): + """Tick the engine. Each tick runs AudManager::Process(), which advances the occlusion fades. + """ + PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=times) + + def GetOcclusion(self): + return self.manager.GetEmitterOcclusion(self.emitter.ID) + + def SetBlockage(self, blockage): + return self.manager.SetEmitterLineOfSightBlockage(self.emitter.ID, blockage) + + def test_occlusion_is_applied_immediately_at_a_zero_fade_rate(self): + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + + self.assertEqual(self.GetOcclusion(), 1.0) + + def test_occlusion_fades_in_gradually(self): + """The value has to interpolate towards its target rather than jumping straight to it.""" + self.manager.obstructionOcclusionFadeRate = SLOW_FADE_RATE + + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + firstValue = self.GetOcclusion() + self.Pump() + secondValue = self.GetOcclusion() + + self.assertGreater(firstValue, 0.0, "Occlusion never started fading in.") + self.assertGreater(secondValue, firstValue, "Occlusion stopped advancing towards its target.") + self.assertLess(secondValue, 1.0, "Occlusion jumped to its target instead of fading.") + + def test_occlusion_fades_back_out_gradually(self): + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 1.0) + + self.manager.obstructionOcclusionFadeRate = SLOW_FADE_RATE + self.assertTrue(self.SetBlockage(0.0)) + self.Pump() + firstValue = self.GetOcclusion() + self.Pump() + secondValue = self.GetOcclusion() + + self.assertLess(firstValue, 1.0, "Occlusion never started fading out.") + self.assertLess(secondValue, firstValue, "Occlusion stopped advancing towards its target.") + self.assertGreater(secondValue, 0.0, "Occlusion jumped to its target instead of fading.") + + def test_a_fade_settles_on_its_target_without_overshooting(self): + """A single tick large enough to cover the whole fade must stop exactly on the target.""" + self.manager.obstructionOcclusionFadeRate = FAST_FADE_RATE + + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + + self.assertEqual(self.GetOcclusion(), 1.0) + + def test_a_fade_out_settles_on_its_target_without_undershooting(self): + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + + self.manager.obstructionOcclusionFadeRate = FAST_FADE_RATE + self.assertTrue(self.SetBlockage(0.0)) + self.Pump() + + self.assertEqual(self.GetOcclusion(), 0.0) + + def test_blockage_is_clamped_to_a_valid_range(self): + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + + self.assertTrue(self.SetBlockage(5.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 1.0) + + self.assertTrue(self.SetBlockage(-1.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + def test_a_negative_fade_rate_is_clamped_to_zero(self): + self.manager.obstructionOcclusionFadeRate = -5.0 + + self.assertEqual(self.manager.obstructionOcclusionFadeRate, 0.0) + + def test_clearing_returns_every_emitter_to_clear(self): + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 1.0) + + self.manager.ClearObstructionOcclusion() + self.Pump() + + self.assertEqual(self.GetOcclusion(), 0.0) + + def test_disabling_clears_existing_values_and_rejects_new_ones(self): + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 1.0) + + self.manager.obstructionOcclusionEnabled = False + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + self.assertFalse(self.SetBlockage(1.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + def test_blockage_is_rejected_for_an_emitter_that_does_not_exist(self): + + unknownEmitterID = self.emitter.ID + 1000000 + + self.assertFalse(self.manager.SetEmitterLineOfSightBlockage(unknownEmitterID, 1.0)) + self.assertEqual(self.manager.GetEmitterOcclusion(unknownEmitterID), 0.0) + + def test_blockage_is_rejected_for_the_listener(self): + """Occlusion is measured relative to the listener, so occluding the listener is meaningless.""" + import audio2 + listenerID = audio2.GetListener().ID + + self.assertFalse(self.manager.SetEmitterLineOfSightBlockage(listenerID, 1.0)) + self.assertEqual(self.manager.GetEmitterOcclusion(listenerID), 0.0) + + def test_occlusion_is_suppressed_while_acoustics_are_enabled(self): + """Acoustics transmission already attenuates, so occlusion must not stack on top of it.""" + self.manager.spatialAudioGeometryEnabled = True + if not self.manager.spatialAudioGeometryEnabled: + self.skipTest("Spatial audio geometry is not available on this platform.") + + try: + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + + self.assertEqual(self.GetOcclusion(), 0.0) + finally: + self.manager.spatialAudioGeometryEnabled = False From 610d4e425307e3337994662e9e6f26d023ddd48a Mon Sep 17 00:00:00 2001 From: phevosccp Date: Wed, 29 Jul 2026 15:36:23 +0000 Subject: [PATCH 25/25] Fix : - first-time blocked emitters now start fully blocked immediately instead of fading in. - keep cleared emitters tracked so later re-blocking fades in, instead of snapping instantly as if they were new. --- CMakeLists.txt | 8 +++++ InstallToMonolith.vcxproj | 7 +++- src/AudObstructionOcclusion.cpp | 18 +++++------ src/AudObstructionOcclusion.h | 3 +- .../test_obstruction_occlusion_exposure.py | 32 +++++++++++++++++++ 5 files changed, 57 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 295a712..36df1f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,6 +189,14 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) RUNTIME DESTINATION bin/${CCP_PLATFORM}/${CCP_ARCHITECTURE}/${CCP_TOOLSET} ) + if(MSVC) + install( + FILES $ + CONFIGURATIONS ${CMAKE_CONFIGURATION_TYPES} + DESTINATION bin/${CCP_PLATFORM}/${CCP_ARCHITECTURE}/${CCP_TOOLSET} + OPTIONAL + ) + endif() # Generate target configuration for the monolith configure_ccp_vendor_config_file( TARGET CarbonAudio diff --git a/InstallToMonolith.vcxproj b/InstallToMonolith.vcxproj index 931fdf8..04fe8fc 100644 --- a/InstallToMonolith.vcxproj +++ b/InstallToMonolith.vcxproj @@ -22,9 +22,10 @@ $(CCP_EVE_PERFORCE_BRANCH_PATH) $([MSBuild]::GetRegistryValue('HKEY_CURRENT_USER\Environment', 'CCP_EVE_PERFORCE_BRANCH_PATH')) - $(MonolithBranchRoot.Replace('\', '/'))/vendor/github.com/ccpgames/carbon-audio/develop + $(MonolithBranchRoot.Replace('\', '/'))/vendor/github.com/carbonengine/audio/develop $(MonolithBranchRoot)\vendor\github.com\carbonengine\audio\develop .cmake-build-monolith + $(ProjectDir)$(MonolithBuildDir)\.vs-installtomonolith\$(Platform)\$(Configuration)\ $(ProjectDir.Replace('\', '/')) setlocal cd /d "$(ProjectDir)" @@ -43,12 +44,16 @@ echo === CarbonAudio installed to $(MonolithInstallPrefix) === + $(VsScratchDir) + $(VsScratchDir) $(MonolithBuildScript) cd /d "$(ProjectDir)" if exist $(MonolithBuildDir) rmdir /s /q $(MonolithBuildDir) +mkdir "$(VsScratchDir)" 2>nul $(MonolithBuildScript) cd /d "$(ProjectDir)" if exist $(MonolithBuildDir) rmdir /s /q $(MonolithBuildDir) +mkdir "$(VsScratchDir)" 2>nul if exist "$(MonolithInstallDirWin)" echo Removing install at: $(MonolithInstallDirWin) if exist "$(MonolithInstallDirWin)" rmdir /s /q "$(MonolithInstallDirWin)" $(ProjectDir)$(MonolithBuildDir)\Internal\_audio2_internal.pyd diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 4985409..3a44e29 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -74,14 +74,6 @@ void AudObstructionOcclusion::Update() state.needsSend = !SendToWwise(emitterID, state); } - // Remove the emitter from the list if it has reached its target values and is no longer needed. - if (!state.needsSend && - state.obstruction.ReachedTarget() && state.obstruction.targetValue == 0.0f && - state.occlusion.ReachedTarget() && state.occlusion.targetValue == 0.0f) - { - it = m_emitters.erase(it); - continue; - } ++it; } @@ -112,11 +104,19 @@ bool AudObstructionOcclusion::SetObstructionOcclusion(AkGameObjectID emitterID, } CcpAutoMutex lock(m_mutex); - EmitterState& state = m_emitters[emitterID]; + + const auto [entry, isNewEmitter] = m_emitters.try_emplace(emitterID); + EmitterState& state = entry->second; state.obstruction.SetTarget(obstruction); state.occlusion.SetTarget(occlusion); + if (isNewEmitter) + { + state.obstruction.SnapToTarget(); + state.occlusion.SnapToTarget(); + } + return true; } diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 5f413d7..cde8039 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -69,7 +69,7 @@ class AudObstructionOcclusion /// Forgets every emitter and the fade clock, for when audio is disabled. void Reset(); - /// Fades every tracked emitter back to clear and keeps them around until they get there. + /// Fades every tracked emitter back to clear. void ClearAll(); /** @@ -95,6 +95,7 @@ class AudObstructionOcclusion void SetTarget(float target); bool Advance(float deltaSeconds, float fadeRate); bool ReachedTarget() const { return currentValue == targetValue; }; + void SnapToTarget() { currentValue = targetValue; }; }; /// Everything we keep for one blocked emitter. diff --git a/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py b/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py index b0aeb9b..5adfe5f 100644 --- a/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py +++ b/tests/python/audiotests/test/test_obstruction_occlusion_exposure.py @@ -44,7 +44,37 @@ def GetOcclusion(self): def SetBlockage(self, blockage): return self.manager.SetEmitterLineOfSightBlockage(self.emitter.ID, blockage) + def EstablishClear(self): + self.assertTrue(self.SetBlockage(0.0)) + self.Pump() + + def test_a_first_value_is_applied_at_once(self): + self.manager.obstructionOcclusionFadeRate = SLOW_FADE_RATE + + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + + self.assertEqual(self.GetOcclusion(), 1.0) + + def test_an_emitter_that_is_back_to_clear_still_fades(self): + + self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + self.assertTrue(self.SetBlockage(0.0)) + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + self.manager.obstructionOcclusionFadeRate = SLOW_FADE_RATE + self.assertTrue(self.SetBlockage(1.0)) + self.Pump() + + occlusion = self.GetOcclusion() + self.assertGreater(occlusion, 0.0, "Occlusion never started fading back in.") + self.assertLess(occlusion, 1.0, "Occlusion jumped to its target instead of fading.") + def test_occlusion_is_applied_immediately_at_a_zero_fade_rate(self): + self.EstablishClear() self.manager.obstructionOcclusionFadeRate = INSTANT_FADE_RATE self.assertTrue(self.SetBlockage(1.0)) @@ -54,6 +84,7 @@ def test_occlusion_is_applied_immediately_at_a_zero_fade_rate(self): def test_occlusion_fades_in_gradually(self): """The value has to interpolate towards its target rather than jumping straight to it.""" + self.EstablishClear() self.manager.obstructionOcclusionFadeRate = SLOW_FADE_RATE self.assertTrue(self.SetBlockage(1.0)) @@ -85,6 +116,7 @@ def test_occlusion_fades_back_out_gradually(self): def test_a_fade_settles_on_its_target_without_overshooting(self): """A single tick large enough to cover the whole fade must stop exactly on the target.""" + self.EstablishClear() self.manager.obstructionOcclusionFadeRate = FAST_FADE_RATE self.assertTrue(self.SetBlockage(1.0))