Skip to content

Bug: SpatialPannerNode passes inverted occlusion values to SteamAudio #6

@KenzonYeoh

Description

@KenzonYeoh

Summary

SpatialPannerNode.ProcessSteamAudio passes the Occlusion AudioParam value directly to IPL.DirectEffectParams.Occlusion without inverting it. GraphAudio and SteamAudio use opposite conventions for this field, so occlusion has no audible effect — setting Sound.Occlusion = 1.0 (fully blocked) tells SteamAudio the sound is fully visible.

Semantic mismatch

Value GraphAudio (Sound.Occlusion) SteamAudio (DirectEffectParams.Occlusion)
0.0 No occlusion (full volume) Fully occluded (silence)
1.0 Fully occluded (silence) No occlusion (full volume)

GraphAudio's convention is documented in Sound.cs:

/// The occlusion factor of the sound, from 0.0 (no occlusion) to 1.0 (fully occluded).

SteamAudio's DirectEffectParams.Occlusion works as a linear gain multiplier — output = input * occlusion. A value of 0.0 produces silence; a value of 1.0 leaves the signal unchanged.

Where the bug is

SpatialPannerNode.cs, line 237:

var directParams = new IPL.DirectEffectParams
{
    // ...
    Occlusion = occlusion   // <-- should be (1.0f - occlusion)
};

The value from Occlusion.GetValues()[0] (GraphAudio's convention: 0=open, 1=blocked) is passed directly to SteamAudio (which expects 0=blocked, 1=open).

Fix

One-line change in SpatialPannerNode.ProcessSteamAudio:

 var directParams = new IPL.DirectEffectParams
 {
     Flags = flags,
     TransmissionType = (flags & IPL.DirectEffectFlags.ApplyTransmission) != 0
         ? IPL.TransmissionType.FrequencyDependent
         : IPL.TransmissionType.FrequencyIndependent,
     DistanceAttenuation = attenuation,
     Directivity = directivityValue,
-    Occlusion = occlusion
+    Occlusion = 1.0f - occlusion
 };

No other code needs to change. The flag-setting logic (if (occlusion > 0.0f) flags |= ApplyOcclusion) remains correct because it's based on GraphAudio's convention (occlusion > 0 means "there is some occlusion to apply"). The transmission values (TransmissionLow/Mid/High) are already in the correct convention (0=opaque, 1=transparent) and don't need inversion.

Verification

A diagnostic test project (GraphAudio.Tests.OcclusionDiag) confirms all of this:

Occlusion semantics (DirectEffectApply):

  • occlusion=1.0 → output unchanged at 1.0 (SteamAudio treats as "not occluded")
  • occlusion=0.0 → output is 0.0 (silence — SteamAudio treats as "fully occluded")
  • occlusion=0.5 → output is 0.5 (half volume)

Transmission (DirectEffectApply):

  • occlusion=0.0, transmission=0.1 → output is 0.1 (formula: occ + (1-occ)*trans = 0 + 1*0.1)

Full Simulator pipeline (Scene + StaticMesh + Simulator + Source):

  • 20x20 wall at z=0, source at (0,0,5), listener at (0,0,-5)
  • Material: absorption=0.9, transmission=[0.1, 0.05, 0.02]
  • Simulator correctly returns: occlusion=0.0 (fully blocked), transmission=[0.1, 0.05, 0.02]
  • DirectEffectApply with these values → output=0.1 (transmitted sound only)

Distance attenuation (control test):

  • distanceAttenuation=0.25 → output is 0.25 (confirms DirectEffect pipeline works)

All SteamAudio functionality (occlusion, transmission, distance attenuation, Simulator raycasting) works correctly. The only issue is the semantic inversion in GraphAudio's SpatialPannerNode.

Workaround

Until this is fixed, consumers can invert the value before passing it to Sound.Occlusion:

// GraphAudio expects 0=open, 1=blocked
// but due to the bug, it passes the value uninverted to SteamAudio
// which interprets 0=blocked, 1=open
// so pre-invert to cancel out the missing inversion:
sound.Occlusion = 1.0f - myOcclusionValue;

Impact

  • Occlusion via Sound.Occlusion has no audible effect in any released version of GraphAudio
  • Transmission via Sound.TransmissionLow/Mid/High has no audible effect either, because the ApplyTransmission flag is only set when occlusion > 0, and with the inverted semantics, occlusion > 0 causes SteamAudio to treat the sound as partially visible — the transmission component is scaled by (1 - occlusion) which approaches 0 as the GraphAudio occlusion value increases, exactly the opposite of the intended behavior
  • Distance attenuation, HRTF/binaural panning, directivity, and spatial blend are unaffected

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions