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
Summary
SpatialPannerNode.ProcessSteamAudiopasses theOcclusionAudioParam value directly toIPL.DirectEffectParams.Occlusionwithout inverting it. GraphAudio and SteamAudio use opposite conventions for this field, so occlusion has no audible effect — settingSound.Occlusion = 1.0(fully blocked) tells SteamAudio the sound is fully visible.Semantic mismatch
Sound.Occlusion)DirectEffectParams.Occlusion)GraphAudio's convention is documented in
Sound.cs:SteamAudio's
DirectEffectParams.Occlusionworks 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: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):
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:Impact
Sound.Occlusionhas no audible effect in any released version of GraphAudioSound.TransmissionLow/Mid/Highhas no audible effect either, because theApplyTransmissionflag is only set whenocclusion > 0, and with the inverted semantics,occlusion > 0causes 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