From 32ff829d3a1e78a619201d61e21e14e26f2abdea Mon Sep 17 00:00:00 2001 From: CCP Zoetrope Date: Wed, 12 Aug 2026 13:56:04 +0000 Subject: [PATCH 1/2] Add a sightline source override for testing occlusion from the ship Traces sightlines from a game-supplied world point instead of the listener while one is set, so occlusion can be A/B tested from the camera versus the player's ship. SetSightlineSource takes raw world doubles like occluder centres; ClearSightlineSource flips back to the listener. Setting or clearing forces a LOS recompute so the switch is immediately audible; per-tick updates of an existing source wait for the throttle like the origin does. --- python/audio2/occlusion.py | 22 ++++ src/AudObstructionOcclusion.cpp | 107 +++++++++++++----- src/AudObstructionOcclusion.h | 34 +++++- src/AudOcclusion.cpp | 22 ++++ src/AudOcclusion.h | 18 +++ src/AudOcclusion_Blue.cpp | 23 ++++ .../test/test_occluder_sphere_exposure.py | 86 ++++++++++++++ .../audiotests/test/test_python_occlusion.py | 24 ++++ 8 files changed, 308 insertions(+), 28 deletions(-) diff --git a/python/audio2/occlusion.py b/python/audio2/occlusion.py index b902824..98ab970 100644 --- a/python/audio2/occlusion.py +++ b/python/audio2/occlusion.py @@ -21,6 +21,10 @@ def ClearOccluderSpheres(self): """Remove every occluder sphere and fade all emitters back to clear.""" self.occlusion.ClearOccluderSpheres() + def ClearSightlineSource(self): + """Return sightlines to starting at the listener.""" + self.occlusion.ClearSightlineSource() + def GetBlockedOcclusion(self): """Return the occlusion applied to an emitter whose line of sight an occluder sphere blocks.""" return self.occlusion.blockedOcclusion @@ -83,6 +87,10 @@ def GetRadiusScale(self): """Return what fraction of an occluder's radius counts as solid.""" return self.occlusion.radiusScale + def HasSightlineSource(self): + """Return whether a sightline source override is currently set.""" + return self.occlusion.HasSightlineSource() + def RemoveOccluderSphere(self, occluderID): """Remove a single occluder sphere. Emitters it was blocking fade back to clear. @@ -165,3 +173,17 @@ def SetRadiusScale(self, scale): :type scale: float """ self.occlusion.radiusScale = scale + + def SetSightlineSource(self, x, y, z): + """Trace sightlines from this game world point instead of from the listener. + + For testing where occlusion should be judged from - the camera or the player's + ship. Feed the ship's world position every tick while the override is wanted; + ClearSightlineSource() flips back to the listener, so switching perspective + mid-flight is one call either way. + + :param x, y, z: The point sightlines start from, in game world space (e.g. the + ship's raw destiny ball position). + :type x, y, z: float + """ + self.occlusion.SetSightlineSource(x, y, z) diff --git a/src/AudObstructionOcclusion.cpp b/src/AudObstructionOcclusion.cpp index 22faa1c..7b3752a 100644 --- a/src/AudObstructionOcclusion.cpp +++ b/src/AudObstructionOcclusion.cpp @@ -17,8 +17,8 @@ namespace /** * @brief A translated occluder ready for a single line-of-sight pass. * - * Carries how far its near surface sits from the listener. That distance is what - * lets an emitter stop scanning once the spheres are farther away than it is. + * Carries how far its near surface sits from the sightline start. That distance is + * what lets an emitter stop scanning once the spheres are farther away than it is. */ struct SortableOccluder { @@ -34,6 +34,10 @@ AudObstructionOcclusion::AudObstructionOcclusion(AudManager* audioManager) : m_originX(0.0), m_originY(0.0), m_originZ(0.0), + m_sightlineSourceX(0.0), + m_sightlineSourceY(0.0), + m_sightlineSourceZ(0.0), + m_hasSightlineSource(false), m_fadeRate(DEFAULT_FADE_RATE), m_blockedOcclusion(DEFAULT_BLOCKED_OCCLUSION), m_occluderRadiusScale(DEFAULT_OCCLUDER_RADIUS_SCALE), @@ -219,6 +223,39 @@ void AudObstructionOcclusion::SetOccluderOrigin(double x, double y, double z) // It is read when line of sight is next computed, which is soon enough. } +void AudObstructionOcclusion::SetSightlineSource(double x, double y, double z) +{ + CcpAutoMutex lock(m_mutex); + // Switching perspective should be audible immediately, but once the override is + // in place it moves with the ship every tick, and reacting to that would defeat + // the throttle just as reacting to the origin would (see SetOccluderOrigin). + if (!m_hasSightlineSource) + { + m_hasComputedLos = false; + } + m_hasSightlineSource = true; + m_sightlineSourceX = x; + m_sightlineSourceY = y; + m_sightlineSourceZ = z; +} + +void AudObstructionOcclusion::ClearSightlineSource() +{ + CcpAutoMutex lock(m_mutex); + if (!m_hasSightlineSource) + { + return; + } + m_hasSightlineSource = false; + m_hasComputedLos = false; +} + +bool AudObstructionOcclusion::HasSightlineSource() const +{ + CcpAutoMutex lock(m_mutex); + return m_hasSightlineSource; +} + void AudObstructionOcclusion::RemoveOccluderSphere(uint64_t occluderID) { CcpAutoMutex lock(m_mutex); @@ -312,13 +349,11 @@ uint64_t AudObstructionOcclusion::GetBlockingOccluder(AkGameObjectID emitterID) return 0; } - Vector3 listenerPosition; - bool listenerIsPlaced = false; - const bool haveListener = m_audioManager->WithCallbackGameObject(LISTENER_GAME_OBJ_ID, - [&listenerPosition, &listenerIsPlaced](AudGameObjResource* listener) { - listenerPosition = listener->GetPosition(); - listenerIsPlaced = listener->HasUsableWorldPosition(); - }); + Vector3 sightlineStart; + if (!GetSightlineStartLocked(sightlineStart)) + { + return 0; + } Vector3 emitterPosition; bool emitterIsPlaced = false; @@ -328,7 +363,7 @@ uint64_t AudObstructionOcclusion::GetBlockingOccluder(AkGameObjectID emitterID) emitterIsPlaced = emitter->HasUsableWorldPosition(); }); - if (!haveListener || !listenerIsPlaced || !haveEmitter || !emitterIsPlaced) + if (!haveEmitter || !emitterIsPlaced) { return 0; } @@ -336,7 +371,7 @@ uint64_t AudObstructionOcclusion::GetBlockingOccluder(AkGameObjectID emitterID) for (const auto& occluderEntry : m_occluders) { const TranslatedOccluder translated = TranslateToAudioSpace(occluderEntry.second); - if (SegmentHitsSphere(listenerPosition, emitterPosition, translated.center, translated.radius)) + if (SegmentHitsSphere(sightlineStart, emitterPosition, translated.center, translated.radius)) { return occluderEntry.first; } @@ -379,16 +414,15 @@ AudObstructionOcclusion::TranslatedOccluder AudObstructionOcclusion::TranslateTo sphere.radius * m_occluderRadiusScale }; } -void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::time_point now) +bool AudObstructionOcclusion::GetSightlineStartLocked(Vector3& start) const { - if (m_hasComputedLos && - std::chrono::duration(now - m_lastLosComputeTime).count() < m_losRecomputeInterval) + if (m_hasSightlineSource) { - return; + start = Vector3(static_cast(m_sightlineSourceX - m_originX), + static_cast(m_sightlineSourceY - m_originY), + static_cast(m_sightlineSourceZ - m_originZ)); + return true; } - m_lastLosComputeTime = now; - m_hasComputedLos = true; - m_blockingOccluderIDs.clear(); Vector3 listenerPosition; bool listenerIsPlaced = false; @@ -399,6 +433,27 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock:: }); // Until a camera drives the listener it sits at the initial spawn position if (!haveListener || !listenerIsPlaced) + { + return false; + } + + start = listenerPosition; + return true; +} + +void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock::time_point now) +{ + if (m_hasComputedLos && + std::chrono::duration(now - m_lastLosComputeTime).count() < m_losRecomputeInterval) + { + return; + } + m_lastLosComputeTime = now; + m_hasComputedLos = true; + m_blockingOccluderIDs.clear(); + + Vector3 sightlineStart; + if (!GetSightlineStartLocked(sightlineStart)) { return; } @@ -431,9 +486,9 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock:: sortable.center = translated.center; sortable.radius = translated.radius; - const Vector3 fromListener = translated.center - listenerPosition; + const Vector3 fromStart = translated.center - sightlineStart; sortable.nearSurfaceDistance = - std::sqrt(Dot(fromListener, fromListener)) - translated.radius; + std::sqrt(Dot(fromStart, fromStart)) - translated.radius; occluders.push_back(sortable); } @@ -449,7 +504,7 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock:: for (const auto& [emitterID, position] : emitters) { - const Vector3 toEmitter = position - listenerPosition; + const Vector3 toEmitter = position - sightlineStart; const float segmentLength = std::sqrt(Dot(toEmitter, toEmitter)); bool blocked = false; @@ -463,7 +518,7 @@ void AudObstructionOcclusion::ComputeSphereOcclusion(std::chrono::steady_clock:: break; } - if (SegmentHitsSphere(listenerPosition, position, occluder.center, occluder.radius)) + if (SegmentHitsSphere(sightlineStart, position, occluder.center, occluder.radius)) { blocked = true; m_blockingOccluderIDs.insert(occluder.occluderID); @@ -499,9 +554,9 @@ bool AudObstructionOcclusion::SegmentHitsSphere(const Vector3& segmentStart, con const Vector3 toCenter = sphereCenter - segmentStart; const float radiusSq = sphereRadius * sphereRadius; - // A sphere the listener is inside is not between the listener and anything. Occluder - // radii are coarse bounding spheres and the player routinely flies inside a large - // one, so counting that as blockage would muffle the entire world at once. + // A sphere the segment's start is inside is not between that point and anything. + // Occluder radii are coarse bounding spheres and the player routinely flies inside + // a large one, so counting that as blockage would muffle the entire world at once. if (Dot(toCenter, toCenter) <= radiusSq) { return false; @@ -512,7 +567,7 @@ bool AudObstructionOcclusion::SegmentHitsSphere(const Vector3& segmentStart, con // division is needed to compare it against the ends. const float centerProjection = Dot(toCenter, segment); - // An emitter inside a sphere needs more care than the listener did, because the two + // An emitter inside a sphere needs more care than the start did, because the two // situations that produce it want opposite answers. A turret or beam emitter mounted // on an object sits inside that object's own bounding sphere while being in plain // sight, and muffling it would be permanent. A sound on the far side of the object, diff --git a/src/AudObstructionOcclusion.h b/src/AudObstructionOcclusion.h index 979a755..6d4022e 100644 --- a/src/AudObstructionOcclusion.h +++ b/src/AudObstructionOcclusion.h @@ -95,6 +95,26 @@ class AudObstructionOcclusion */ void SetOccluderOrigin(double x, double y, double z); + /** + * @brief Traces sightlines from this game world point instead of from the listener. + * + * For testing where occlusion should be judged from - the camera or the player's + * ship. The game feeds the ship's world position every tick while the override is + * wanted; @c ClearSightlineSource() flips back to the listener, so switching + * perspective mid-flight is one call either way. Like occluder centres, the point + * is world-space doubles translated against the origin each time line of sight is + * computed. + */ + void SetSightlineSource(double x, double y, double z); + + /** + * @brief Returns sightlines to starting at the listener. + */ + void ClearSightlineSource(); + + /// Whether a sightline source override is currently set. + bool HasSightlineSource() const; + /** * @brief Removes a single occluder sphere. Emitters it was blocking fade back to clear. */ @@ -207,6 +227,11 @@ class AudObstructionOcclusion /// current origin. Callers must hold m_mutex. TranslatedOccluder TranslateToAudioSpace(const OccluderSphere& sphere) const; + /// Where sightlines start: the translated source override if one is set, otherwise + /// the listener's position. Returns false when there is no usable start (no + /// override and the listener is missing or unplaced). Callers must hold m_mutex. + bool GetSightlineStartLocked(Vector3& start) const; + bool SendToWwise(AkGameObjectID emitterID, const EmitterState& state) const; /// Fade every tracked emitter back to clear. Callers must hold m_mutex. @@ -224,8 +249,8 @@ class AudObstructionOcclusion /// All three positions are in audio space. /// /// Not a pure intersection test at the endpoints, and deliberately asymmetric: a - /// sphere containing the listener never blocks, while one containing the emitter - /// blocks only if the emitter is past the deepest point of the crossing. + /// sphere containing the segment's start never blocks, while one containing the + /// emitter blocks only if the emitter is past the deepest point of the crossing. static bool SegmentHitsSphere(const Vector3& segmentStart, const Vector3& segmentEnd, const Vector3& sphereCenter, float sphereRadius); @@ -244,6 +269,11 @@ class AudObstructionOcclusion double m_originX; double m_originY; double m_originZ; + // An optional game world point sightlines start from instead of the listener. + double m_sightlineSourceX; + double m_sightlineSourceY; + double m_sightlineSourceZ; + bool m_hasSightlineSource; float m_fadeRate; float m_blockedOcclusion; float m_occluderRadiusScale; diff --git a/src/AudOcclusion.cpp b/src/AudOcclusion.cpp index 997dd19..45d3737 100644 --- a/src/AudOcclusion.cpp +++ b/src/AudOcclusion.cpp @@ -101,6 +101,28 @@ void AudOcclusion::SetOrigin( double x, double y, double z ) } } +void AudOcclusion::SetSightlineSource( double x, double y, double z ) +{ + if( AudObstructionOcclusion* occlusion = Occlusion() ) + { + occlusion->SetSightlineSource( x, y, z ); + } +} + +void AudOcclusion::ClearSightlineSource() +{ + if( AudObstructionOcclusion* occlusion = Occlusion() ) + { + occlusion->ClearSightlineSource(); + } +} + +bool AudOcclusion::HasSightlineSource() const +{ + AudObstructionOcclusion* occlusion = Occlusion(); + return occlusion != nullptr ? occlusion->HasSightlineSource() : false; +} + void AudOcclusion::RemoveOccluderSphere( uint64_t occluderID ) { if( AudObstructionOcclusion* occlusion = Occlusion() ) diff --git a/src/AudOcclusion.h b/src/AudOcclusion.h index 851274c..310dd40 100644 --- a/src/AudOcclusion.h +++ b/src/AudOcclusion.h @@ -98,6 +98,24 @@ BLUE_CLASS( AudOcclusion ) : public IRoot */ void SetOrigin( double x, double y, double z ); + /** + * @brief Traces sightlines from this game world point instead of from the listener. + * + * For testing where occlusion should be judged from - the camera or the player's + * ship. Feed the ship's world position every tick while the override is wanted; + * @c ClearSightlineSource() flips back to the listener, so switching perspective + * mid-flight is one call either way. + */ + void SetSightlineSource( double x, double y, double z ); + + /** + * @brief Returns sightlines to starting at the listener. + */ + void ClearSightlineSource(); + + /// Whether a sightline source override is currently set. + bool HasSightlineSource() const; + /** * @brief Removes a single occluder sphere. Emitters it was blocking fade back to clear. */ diff --git a/src/AudOcclusion_Blue.cpp b/src/AudOcclusion_Blue.cpp index 4523497..a74325a 100644 --- a/src/AudOcclusion_Blue.cpp +++ b/src/AudOcclusion_Blue.cpp @@ -43,6 +43,29 @@ const Be::ClassInfo* AudOcclusion::ExposeToBlue() " (in EVE, the ego ball's position)." ) MAP_METHOD_AND_WRAP + ( + "SetSightlineSource", + SetSightlineSource, + "Trace sightlines from this game world point instead of from the listener. For testing\n" + "where occlusion should be judged from - the camera or the player's ship. Feed the ship's\n" + "world position every tick while the override is wanted; ClearSightlineSource() flips back\n" + "to the listener.\n" + ":param x, y, z: The point sightlines start from, in game world space (e.g. the\n" + " ship's raw destiny ball position)." + ) + MAP_METHOD_AND_WRAP + ( + "ClearSightlineSource", + ClearSightlineSource, + "Return sightlines to starting at the listener." + ) + MAP_METHOD_AND_WRAP + ( + "HasSightlineSource", + HasSightlineSource, + "Whether a sightline source override is currently set." + ) + MAP_METHOD_AND_WRAP ( "RemoveOccluderSphere", RemoveOccluderSphere, diff --git a/tests/python/audiotests/test/test_occluder_sphere_exposure.py b/tests/python/audiotests/test/test_occluder_sphere_exposure.py index 6cba119..e5bb3d7 100644 --- a/tests/python/audiotests/test/test_occluder_sphere_exposure.py +++ b/tests/python/audiotests/test/test_occluder_sphere_exposure.py @@ -20,6 +20,11 @@ # On the segment's line but beyond the emitter. BEYOND_EMITTER_SPHERE = (0, 0, 150) +# A sightline source past BEYOND_EMITTER_SPHERE, looking back through it at the emitter. +SOURCE_BEYOND_SPHERE = (0, 0, 200) +# A sightline source with a clear line to the emitter. +SOURCE_BESIDE_EMITTER = (0, 200, 100) + SPHERE_RADIUS = 10.0 OCCLUDER_ID = 1 @@ -64,6 +69,7 @@ def setUp(self): def tearDown(self): self.occlusion.SetOrigin(0.0, 0.0, 0.0) + self.occlusion.ClearSightlineSource() self.occlusion.ClearOccluderSpheres() self.emitter = None self.listener = None @@ -444,3 +450,83 @@ def test_a_moved_origin_waits_for_the_recompute_throttle(self): self.Pump() self.assertEqual(self.GetOcclusion(), self.occlusion.blockedOcclusion) + + def test_a_sightline_source_replaces_the_listener_as_the_segment_start(self): + """A sphere beyond the emitter is clear from the listener, but a source past the + sphere looks back through it, so the same layout blocks once the source is set.""" + self.SetSphere(BEYOND_EMITTER_SPHERE) + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + self.occlusion.SetSightlineSource(*SOURCE_BEYOND_SPHERE) + self.Pump() + + self.assertEqual(self.GetOcclusion(), self.occlusion.blockedOcclusion) + self.assertEqual(self.occlusion.GetBlockingOccluder(self.emitter.ID), OCCLUDER_ID) + + def test_clearing_the_sightline_source_returns_to_the_listener(self): + self.assertFalse(self.occlusion.HasSightlineSource()) + + self.SetSphere(BEYOND_EMITTER_SPHERE) + self.occlusion.SetSightlineSource(*SOURCE_BEYOND_SPHERE) + self.Pump() + self.assertTrue(self.occlusion.HasSightlineSource()) + self.assertGreater(self.GetOcclusion(), 0.0) + + self.occlusion.ClearSightlineSource() + self.Pump() + + self.assertFalse(self.occlusion.HasSightlineSource()) + self.assertEqual(self.GetOcclusion(), 0.0) + + def test_the_sightline_source_is_given_in_game_world_space(self): + """The source arrives in the same coordinates as occluder centres, so a ship + position at solar system range only lines up once the origin relates the two.""" + origin = (5524469439110.0, 15879459987.0, 1989687970679.0) + world_source = (origin[0] + SOURCE_BEYOND_SPHERE[0], + origin[1] + SOURCE_BEYOND_SPHERE[1], + origin[2] + SOURCE_BEYOND_SPHERE[2]) + world_sphere = (origin[0] + BEYOND_EMITTER_SPHERE[0], + origin[1] + BEYOND_EMITTER_SPHERE[1], + origin[2] + BEYOND_EMITTER_SPHERE[2]) + + self.occlusion.SetOrigin(*origin) + self.occlusion.SetSightlineSource(*world_source) + self.SetSphere(world_sphere) + self.Pump() + + self.assertEqual(self.GetOcclusion(), self.occlusion.blockedOcclusion) + + def test_flipping_the_sightline_source_is_not_delayed_by_the_recompute_throttle(self): + """Switching perspective is done by ear mid-test, so setting or clearing the + source must be audible on the next update however long the interval is.""" + self.occlusion.losRecomputeInterval = LONG_RECOMPUTE_INTERVAL + + self.SetSphere(BEYOND_EMITTER_SPHERE) + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + self.occlusion.SetSightlineSource(*SOURCE_BEYOND_SPHERE) + self.Pump() + self.assertEqual(self.GetOcclusion(), self.occlusion.blockedOcclusion) + + self.occlusion.ClearSightlineSource() + self.Pump() + self.assertEqual(self.GetOcclusion(), 0.0) + + def test_moving_an_existing_sightline_source_waits_for_the_recompute_throttle(self): + """Once set, the source follows the ship every tick, so like the origin it must + not force a recompute; it is picked up by the next scheduled pass.""" + self.occlusion.losRecomputeInterval = LONG_RECOMPUTE_INTERVAL + + self.occlusion.SetSightlineSource(*SOURCE_BEYOND_SPHERE) + self.SetSphere(BEYOND_EMITTER_SPHERE) + self.Pump() + self.assertEqual(self.GetOcclusion(), self.occlusion.blockedOcclusion) + + # A vantage the sphere would not block, which the throttle is expected + # to postpone reacting to. + self.occlusion.SetSightlineSource(*SOURCE_BESIDE_EMITTER) + self.Pump() + + self.assertEqual(self.GetOcclusion(), self.occlusion.blockedOcclusion) diff --git a/tests/python/audiotests/test/test_python_occlusion.py b/tests/python/audiotests/test/test_python_occlusion.py index c2f7ade..f36fdfa 100644 --- a/tests/python/audiotests/test/test_python_occlusion.py +++ b/tests/python/audiotests/test/test_python_occlusion.py @@ -64,6 +64,7 @@ def tearDown(self): # These settings are global, so hand them back as they were found rather than # leaking this class's values into whatever runs next. self.occlusion.SetOrigin(0.0, 0.0, 0.0) + self.occlusion.ClearSightlineSource() self.occlusion.ClearOccluderSpheres() self.occlusion.SetEnabled(self.defaults["enabled"]) self.occlusion.SetFadeRate(self.defaults["fadeRate"]) @@ -158,6 +159,29 @@ def test_the_origin_can_be_read_back_through_the_wrapper(self): self.assertEqual(self.occlusion.GetOrigin(), origin) + def test_the_sightline_source_flips_through_the_wrapper(self): + """End to end: a sphere beyond the emitter blocks nothing from the listener, but + does from a source past it, and clearing the source flips straight back.""" + # On the segment's line but beyond the emitter, then a source past it again. + beyondSphere = (0, 0, 150) + sourceBeyond = (0, 0, 200) + + self.assertFalse(self.occlusion.HasSightlineSource()) + + self.SetSphere(beyondSphere) + self.occlusion.SetSightlineSource(*sourceBeyond) + self.Pump() + + self.assertTrue(self.occlusion.HasSightlineSource()) + self.assertEqual(self.occlusion.GetEmitterOcclusion(self.emitter.ID), + self.occlusion.GetBlockedOcclusion()) + + self.occlusion.ClearSightlineSource() + self.Pump() + + self.assertFalse(self.occlusion.HasSightlineSource()) + self.assertEqual(self.occlusion.GetEmitterOcclusion(self.emitter.ID), 0.0) + def test_the_wrapper_is_a_view_onto_the_one_occlusion_subsystem(self): """The wrapper must not stand up state of its own: a second wrapper, and the Blue object itself, have to see what this one did.""" From 2439bce02dea5d996bf9ed11824ce8245c14c31a Mon Sep 17 00:00:00 2001 From: phevosccp Date: Thu, 13 Aug 2026 10:33:26 +0000 Subject: [PATCH 2/2] flaky test fix --- .../python/audiotests/test/test_audmanager_exposure.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/python/audiotests/test/test_audmanager_exposure.py b/tests/python/audiotests/test/test_audmanager_exposure.py index 104ba94..81ed410 100644 --- a/tests/python/audiotests/test/test_audmanager_exposure.py +++ b/tests/python/audiotests/test/test_audmanager_exposure.py @@ -90,8 +90,9 @@ def test_audmanager_sends_events_sent_while_loading_after_complete(self): emitter1 = audio2.AudEmitter("emitter1") self.audioManager.LoadBank(COMMON_BNK) self.audioManager.LoadBank(LOOP_BNK) - self.assertEqual(emitter1.SendEvent(LOOP_EVENT), 0) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + self.assertEqual(emitter1.SendEvent(LOOP_EVENT), 0) + self.assertTrue(PumpOSWithTimeout(lambda: not emitter1.GetPlayingEvents(), maxTries=20), + "Timed out waiting for the queued event to be resent after the SoundBank loaded.") self.assertEqual(list(emitter1.GetPlayingEvents().values())[0], LOOP_EVENT) def test_audmanager_sends_events_sent_while_loading_after_complete_with_prefix(self): @@ -104,8 +105,9 @@ def test_audmanager_sends_events_sent_while_loading_after_complete_with_prefix(s self.audioManager.LoadBank(COMMON_BNK) self.audioManager.LoadBank(LOOP_BNK) # Send event without Play_ at the beginning, as the audio emitter will prepend it for us. - self.assertEqual(prefixEmitter.SendEvent(LOOP_EVENT.replace(eventPrefix, "")), 0) - PumpOSWithTimeout(self.alwaysTrueBoolean, maxTries=3) + self.assertEqual(prefixEmitter.SendEvent(LOOP_EVENT.replace(eventPrefix, "")), 0) + self.assertTrue(PumpOSWithTimeout(lambda: not prefixEmitter.GetPlayingEvents(), maxTries=20), + "Timed out waiting for the queued event to be resent after the SoundBank loaded.") # The event, at the send, should come out with the full prefix (e.g. Play_TestLoop) self.assertEqual(list(prefixEmitter.GetPlayingEvents().values())[0], LOOP_EVENT)