Skip to content

feat(audio): route SoundGraph voices through the 3D spatializer (#424) - #471

Merged
drsnuggles8 merged 3 commits into
masterfrom
feature/audio-soundgraph-spatializer
Jun 29, 2026
Merged

drsnuggles8 merged 3 commits into
masterfrom
feature/audio-soundgraph-spatializer

Conversation

@drsnuggles8

@drsnuggles8 drsnuggles8 commented Jun 29, 2026 •

Copy link
Copy Markdown
Owner

What & why

Closes the gap in #424: a SoundGraph voice was not 3D-positioned at all. SoundGraphSound::SetLocation / SetVelocity / SetOrientation only stored the value — nothing reached a spatializer.

Audio::DSP::Spatializer already did VBAP panning + distance/cone attenuation, but its InitSource was built around an ma_engine_node (it reads resampler.channels and drives the engine node's built-in spatializer.dopplerPitch) and was wired to nothing in production. A SoundGraphSource, by contrast, attaches a bare ma_node straight to the engine endpoint — the crux mismatch this PR resolves.

Changes

  • Spatializer — add InitSource(ma_node_base*) beside the existing InitSource(ma_engine_node*); both delegate to a shared InitSourceInternal. The Doppler sink (ma_engine_node*) is now nullable and the doppler-pitch writes (audio callback + UpdatePositionalData) are null-guarded, so a bare node still gets full VBAP panning + distance/cone attenuation (computed inside the spatializer node) while Doppler — which needs the engine node's resampler — is skipped. ReleaseSource re-routes via a new attachedNode field. The engine-node path is unchanged.
  • SoundGraphSource — hosts a per-voice spatializer node: RegisterSpatializer (inserts between the SoundGraph node and the endpoint, allocates a process-unique sourceID), UpdateSpatialPosition, UnregisterSpatializer (also from Shutdown, before ma_node_uninit).
  • SoundGraphSound — routes the three 3D setters through Spatializer::UpdateSourcePosition; registers the spatializer in InitializeAudioCallback; removes the deferral comment.
  • Scene — feeds the active listener pose into Spatializer::UpdateListener (init + per-frame) and drives per-frame SoundGraph voice positions in OnUpdateRuntime.

Spatialization-enabled ownership (scope item 3)

Owned at runtime by SoundGraphSound::m_SpatializationEnabled (default on), not a serialized AudioSoundGraphComponent field — deliberately, to avoid a SceneSerializer.cpp change that would collide with the in-flight #451 codegen work. A serialized per-voice toggle (to allow 2D SoundGraph voices) is the natural follow-up once #451 lands.

Tests

AudioSoundGraphSpatializerTest (classified unit, device-free ma_engine via config.noDevice): pins that the bare SoundGraph node hosts a spatializer node, and that position drives GetCurrentDistance / GetCurrentDistanceAttenuation correctly (geometry-exact distance, monotonic attenuation, full gain at the listener, listener-move recompute, clean release). All 5 pass; the surrounding 155 audio/SoundGraph/VBAP tests and the AudioRuntimeTicksViaScene Functional test still pass.

Note: audio spatialization can't be visually/audibly inspected — the deterministic distance/attenuation math (CPU-side, via the spatializer getters) is the strongest available evidence, plus the no-crash Scene-tick path.

Refs #424

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added optional per-voice 3D spatial audio for sound graph voices, with controls to enable/disable spatialization and query spatialized status/source ID.
    • Spatial audio now initializes and tracks the listener pose from scene transforms, and voices sync position/velocity/orientation (including up) when changed.
  • Bug Fixes
    • Improved safety for Doppler and pitch updates when required graph nodes aren’t present.
    • Hardened spatial updates against invalid inputs and fixed spatializer registration/release behavior to avoid leaks and stale routing.
  • Tests
    • Added coverage for spatializer routing, distance/cone attenuation behavior, listener-relative updates, and correct unregister/register semantics.

SoundGraph voices previously played non-positional: SoundGraphSound's
SetLocation/SetVelocity/SetOrientation only stored the value, never reaching
a spatializer. Audio::DSP::Spatializer existed but was built around an
ma_engine_node (it reads resampler.channels and drives the engine node's
built-in spatializer.dopplerPitch) and was wired to nothing, whereas a
SoundGraphSource attaches a bare ma_node straight to the engine endpoint.

Teach the spatializer to host a bare node:

- Spatializer: add InitSource(ma_node_base*) beside the existing
  InitSource(ma_engine_node*); both delegate to a shared InitSourceInternal.
  The Doppler sink (ma_engine_node*) is now nullable and the doppler-pitch
  writes are null-guarded, so a bare node still gets full VBAP panning +
  distance/cone attenuation (computed inside the spatializer node) while
  Doppler is skipped. ReleaseSource re-routes via a new attachedNode field.
- SoundGraphSource: host a per-voice spatializer node — RegisterSpatializer
  (insert between the SoundGraph node and the endpoint, allocate a
  process-unique sourceID), UpdateSpatialPosition, UnregisterSpatializer
  (also from Shutdown, before ma_node_uninit).
- SoundGraphSound: route SetLocation/SetVelocity/SetOrientation through
  Spatializer::UpdateSourcePosition; register the spatializer in
  InitializeAudioCallback. Spatialization-enabled is a runtime-only flag
  (default on) — deliberately not a serialized AudioSoundGraphComponent
  field, to avoid a SceneSerializer change.
- Scene: feed the active listener pose into Spatializer::UpdateListener
  (init + per-frame) and drive per-frame SoundGraph voice positions.

Test: AudioSoundGraphSpatializerTest (unit, device-free ma_engine) pins the
bare-node hosting and position -> distance/attenuation via the spatializer's
CPU-side getters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6620b491-b080-43c9-8210-ce1632c47f9f

📥 Commits

Reviewing files that changed from the base of the PR and between 3b089be and 05b2e8d.

📒 Files selected for processing (4)
  • OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.cpp
  • OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSource.cpp
  • OloEngine/src/OloEngine/Scene/Scene.cpp
  • OloEngine/tests/AudioSoundGraphSpatializerTest.cpp

📝 Walkthrough

Walkthrough

Adds per-voice 3D spatialization to SoundGraphSource and SoundGraphSound, extends Spatializer with a bare ma_node_base* init path, syncs listener pose updates from Scene, and adds headless tests for registration, updates, and teardown.

Changes

SoundGraph 3D Spatialization

Layer / File(s) Summary
Spatializer bare-node API and release fixes
OloEngine/src/OloEngine/Audio/DSP/Spatializer/Spatializer.h, OloEngine/src/OloEngine/Audio/DSP/Spatializer/Spatializer.cpp
Adds InitSource(ma_node_base*) and InitSourceInternal with optional Doppler sink handling; stores attached upstream nodes; routes output attachment through the provided source node; guards Doppler writes; and clears node state plus channel-converter heap state on release.
SoundGraphSource spatialization lifecycle
OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSource.h, OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSource.cpp
Adds spatializer registration, unregistration, and position updates; allocates monotonic source IDs; unregisters before node teardown; and rejects non-finite or degenerate transforms before forwarding to the spatializer.
SoundGraphSound spatial setters and registration
OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.h, OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.cpp
Registers a spatializer node during audio initialization when enabled, stores spatial state including up vectors, validates and syncs location/velocity/orientation updates, and exposes spatialization and source-ID queries.
Scene listener pose init and per-frame SoundGraph updates
OloEngine/src/OloEngine/Scene/Scene.cpp
Sanitizes listener basis vectors, seeds the spatializer listener pose during audio runtime initialization, keeps the listener pose synchronized each frame, and updates each SoundGraph sound from its entity transform.
Headless spatializer integration tests
OloEngine/tests/AudioSoundGraphSpatializerTest.cpp, OloEngine/tests/CMakeLists.txt
Adds the new device-free GoogleTest fixture and test cases for registration, distance and attenuation behavior, co-location clamping, listener movement, unregister cleanup, and CMake test wiring.

Possibly related issues

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: routing SoundGraph voices through the 3D spatializer.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

Spatializer::ReleaseSource uninitialized the spatializer node and VBAP but
never freed the ma_channel_converter that InitSource allocates, leaking ~72
bytes per source. The leak was latent — nothing called InitSource/ReleaseSource
before #424 — and surfaced as a LeakSanitizer failure on the new
AudioSoundGraphSpatializerTest (Linux/Clang ASan+LSan; Windows/MSVC doesn't run
LSan, so it passed locally). Add ma_channel_converter_uninit; it no-ops safely
on a never-initialized (zeroed) converter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.cpp`:
- Around line 532-551: In SoundGraphSound::SetLocation, SetVelocity, and
SetOrientation, reject invalid spatial vectors before mutating member state or
calling SyncSpatialPositionToSource(). Add finite-value checks with
std::isfinite for the glm::vec3 inputs, and in SetOrientation also guard against
degenerate forward/up data before assigning m_Position, m_Velocity,
m_Orientation, or m_Up. Use these methods as the fix point so only valid vectors
are cached and forwarded to the spatializer.

In `@OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSource.cpp`:
- Around line 388-392: UpdateSpatialPosition currently forwards the transform
directly into the spatializer, so add a guard in
SoundGraphSource::UpdateSpatialPosition before calling
m_Spatializer->UpdateSourcePosition. Validate the transform’s direction/up
vectors for finiteness and reject degenerate values (zero-length or invalid
vectors) using the project’s floating-point finite checks before forwarding.
Keep the existing m_SpatializerRegistered and m_Spatializer checks, then only
call UpdateSourcePosition when the spatial transform is valid.

In `@OloEngine/src/OloEngine/Scene/Scene.cpp`:
- Around line 592-598: The listener/source basis vectors used in Scene::Update
and related orientation paths are normalized without checking for finite values
or zero length. In the code that builds Audio::Transform and calls
Spatializer::UpdateListener / SetOrientation, validate the inverted transform
basis vectors (and any derived forward/right/up vectors) with std::isfinite and
length checks, then fall back to canonical axes if the transform is singular or
non-finite before normalizing. Apply the same guard in the other affected
orientation sites so AudioEngine::GetSpatializer() never receives NaN/invalid
basis vectors.

In `@OloEngine/tests/AudioSoundGraphSpatializerTest.cpp`:
- Around line 202-205: The re-registration check in
AudioSoundGraphSpatializerTest should verify that a new source ID is actually
different from the previously released one, not just non-zero. Capture the
original m_Source.GetSpatializerSourceID() before release, then after calling
m_Source.RegisterSpatializer(&m_Spatializer, config) assert the new
GetSpatializerSourceID() is not equal to the saved value and still not zero.
This should be done in the test around m_Source.RegisterSpatializer and
GetSpatializerSourceID so the allocator cannot recycle the same ID without being
detected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 621bf027-e3cd-4846-9a9a-47cfc0e53aa7

📥 Commits

Reviewing files that changed from the base of the PR and between 21a8d89 and 3b089be.

📒 Files selected for processing (9)
  • OloEngine/src/OloEngine/Audio/DSP/Spatializer/Spatializer.cpp
  • OloEngine/src/OloEngine/Audio/DSP/Spatializer/Spatializer.h
  • OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.cpp
  • OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSound.h
  • OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSource.cpp
  • OloEngine/src/OloEngine/Audio/SoundGraph/SoundGraphSource.h
  • OloEngine/src/OloEngine/Scene/Scene.cpp
  • OloEngine/tests/AudioSoundGraphSpatializerTest.cpp
  • OloEngine/tests/CMakeLists.txt

Comment thread OloEngine/src/OloEngine/Scene/Scene.cpp Resolved
@drsnuggles8
drsnuggles8 merged commit 47059b9 into master Jun 29, 2026
1 check failed
@drsnuggles8
drsnuggles8 deleted the feature/audio-soundgraph-spatializer branch June 29, 2026 19:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant