Guidance / Cookbook
Ambient Propagation
New in v2.3.0. Zone/portal routing for long-running ambient beds (rain, wind, machinery, river). The propagation subsystem ships alongside the SFX event pipeline — both systems coexist on the same AudioManager and mixer. Use SFX events for discrete sounds, propagation for persistent environmental beds that need to route through geometry.
🧱 Two propagation paths, only one needs slots.
AmbientSourcepath (the recipes in this section: rain, doors, weather) uses its ownAmbientEmitterpool. No occlusion slots required. Author zones, portals, and ambient sources and you’re done.- Container
Use Propagationpath (anAudioContainerwith theUse Propagationflag — see Compose Occlusion with Propagation in the Occlusion topic) acquires an Occlusion Slot exactly like reverb sends do. That path needs the Occlusion Slot Pool authored first.
⚠️ Silent failures to know up front
- No
Audio Listener Zone Trackeron the player object (the one with the Collider), or it’s on the camera that holds the AudioListener instead → listener’s zone is always null, all propagation is silent.- Portal collider doesn’t overlap both zone colliders by ≥ 5 cm → portal is rejected in
OnValidate. Check the inspector for the red error.- Portal +Z not pointing through the opening → blend axis is wrong, sound doesn’t pan toward the gap.
- Loop boundary click in your clip → the emitter loops as-authored, Unity won’t fix a bad seam. Crossfade the WAV.
- Outdoor source zone with finite
activationRadius→ source culls when the listener walks deep indoors. Usefloat.PositiveInfinityfor sky/world sources.
Setup Path
For each ambient bed you want, follow these in order:
- Define Zones — author one
AudioZoneper acoustic space. - Connect with Portals — one
AudioPortalper doorway/window between zones. - Add an Ambient Source — looping clip + source zone.
- (Optional) Door Source — drive a portal’s open/closed state from a game system.
- (Optional) Intensity Driver — script-control the source’s base volume.
Scene prerequisites (one-time): AudioManager + AudioListener in the scene; an Ambience mixer group (or any group of your choice); the player object (the one with the Collider, not the camera) has the Audio Listener Zone Tracker component. Recipes below assume these are in place.
Recipe: Rain That Leaks Through a Window
📋 What: Outdoor rain audible inside a room only through an open window — quieter, dull, and panned toward the window as the listener walks along the opposite wall.
🎯 Use Case: Any ambient bed originating outside a closed interior — weather, traffic, a distant waterfall.
Scene Setup
1. Two AudioZones:
Zone_Outdoors— huge BoxCollider enveloping the outdoor spaceZone_LivingRoom— BoxCollider filling the room interior- On each: Add →
Audio/Propagation/Audio Zone - For the outdoor zone, set
activationRadius = Infinity(so the source doesn’t get culled when the player is deep inside the house)
2. One AudioPortal at the window:
- Empty GameObject at the window frame
- BoxCollider sized ~1m × 1.2m × 0.4m spanning the opening
- Add →
Audio/Propagation/Audio Portal - Drag
Zone_OutdoorsintoZoneA,Zone_LivingRoomintoZoneB - Orient the GameObject so its +Z points through the opening
- Inspector settings:
Base Transmission = 0.6(open window ≈ -4 dB)Closed Transmission = 0.1(≈ -20 dB; doesn’t matter if Door Source is null)Open Low Pass Hz = 12000Closed Low Pass Hz = 600- Leave
Door Sourcenull — windows don’t swing
3. The rain source:
- Empty GameObject anywhere outside (say, above the house roof)
- Add →
Audio/Propagation/Ambient Source - Inspector:
Source Zone = Zone_OutdoorsLooping Clip = Rain_Heavy_Loop.wav(must be loop-authored!)Ambience Mixer Group = Ambience(any mixer group you like)Source Base Volume Db = 0
Code
None needed. The subsystem self-boots:
PropagationManagerauto-instantiates on the firstAudioZone.OnEnable- The listener tracker enters/exits zones on trigger crossings
- The solver runs at 15 Hz and drives the pooled
AmbientEmitter
✅ Result:
- Standing outside → rain plays flat at 0 dB, no filtering
- Walking indoors → rain muffles down to ~-20 dB, high-frequency content rolled off by the window
- As you walk past the window wall, the virtual emitter slides along the frame so the rain pans naturally
Recipe: Door That Swings Between Muffled and Clear
📋 What: A bedroom door connecting a hallway to the street. As the door animates open/closed, propagation parameters interpolate smoothly in real time.
🎯 Use Case: Any door (physical or magical) that can change state during gameplay.
Scene Setup
1. Three zones (same pattern as above): Zone_Street, Zone_Hallway, Zone_Bedroom.
2. Two portals:
- Portal at the bedroom door → connects
Zone_Hallway↔Zone_Bedroom, has a Door Source - Portal at the front entrance → connects
Zone_Street↔Zone_Hallway, can have its own Door Source or be null
3. The door adapter: Propagation doesn’t know about your door system. Write a thin adapter that exposes OpenProgress and fires OnChanged when the state changes:
using System;
using UnityEngine;
using AudioSystem.Propagation;
[RequireComponent(typeof(Animator))]
public class AnimatorDoorToPortal : MonoBehaviour, IPortalDoorSource
{
[SerializeField] private string openParameterName = "openAmount";
private Animator animator;
private float lastReported = -1f;
public float OpenProgress => animator != null
? Mathf.Clamp01(animator.GetFloat(openParameterName))
: 0f;
public event Action OnChanged;
void Awake() => animator = GetComponent<Animator>();
void Update()
{
// Only fire OnChanged on meaningful deltas — the portal re-solves
// immediately whenever we raise this event, so don't spam every frame.
float now = OpenProgress;
if (Mathf.Abs(now - lastReported) >= 0.02f)
{
lastReported = now;
OnChanged?.Invoke();
}
}
}
4. Hook it up: Drop the AnimatorDoorToPortal component on whatever GameObject drives your door (usually the door model root). Drag it into the bedroom AudioPortal’s Door Source field.
Code
// Nothing — the door animation alone drives audio.
// If you want to script-toggle the door:
void OpenDoor() => GetComponent<Animator>().SetBool("open", true);
void CloseDoor() => GetComponent<Animator>().SetBool("open", false);
✅ Result:
- Door closed → bedroom sits at near-silent muffled roar (~-26 dB + 600 Hz low-pass applied to the already-filtered path)
- Door animates open → transmission and cutoff interpolate every frame the animator changes
openAmount - Door slams closed mid-step → smooth attenuation, no pops
Gotcha: If your animator parameter is stepped (0/1 only), the portal transitions will still be smooth on the audio side thanks to per-frame emitter smoothing — but they’ll be less perceptually natural than an animation that ramps. Prefer a continuous float parameter on the animator when possible.
Recipe: L-Shaped Room with Multiple Colliders
📋 What: A corridor that bends at a right angle, or a room with an alcove. Single BoxCollider won’t cover the shape cleanly.
🎯 Use Case: Any non-rectangular acoustic space.
Scene Setup
- On the AudioZone GameObject, add multiple BoxCollider components (one for each leg of the L, each with
isTrigger = true) - Size them to cover the real interior. A small overlap at the corner is fine —
ContainsPointis OR-across all colliders - Do NOT add multiple
AudioZonecomponents; just more colliders on the same GameObject
GameObject "Zone_LShapedCorridor"
├─ AudioZone (single component)
├─ BoxCollider (horizontal leg)
└─ BoxCollider (vertical leg)
✅ Result: The zone behaves as one logical room. Authoring error detection (OnValidate) treats all triggers uniformly. Portals that overlap any of the zone’s colliders are accepted.
Future-proofing: AudioZone.ContainsPoint and ClosestSurfacePoint are virtual — if you later need sphere or mesh shapes, subclass AudioZone and override those two methods. No changes to the solver or manager required.
Recipe: Weather Intensity Drives Rain Volume
📋 What: A Weather_RainIntensity global value (0..1) smoothly crossfades the rain bed from silent to 0 dB.
🎯 Use Case: Dynamic weather systems, seasonal changes, gameplay intensity scaling.
Code
using UnityEngine;
using AudioSystem.Propagation;
public class RainIntensityDriver : MonoBehaviour
{
[SerializeField] private AmbientSource rainSource;
[Range(-60f, 0f)] [SerializeField] private float quietDb = -40f;
[Range(-60f, 0f)] [SerializeField] private float loudDb = 0f;
/// <summary>Call this from your weather / gameplay system. 0..1.</summary>
public void SetRainIntensity(float t)
{
if (rainSource == null) return;
float db = Mathf.Lerp(quietDb, loudDb, Mathf.Clamp01(t));
rainSource.SetBaseVolumeDb(db);
}
}
The base volume is picked up on the next solve tick (15 Hz default ≈ 67 ms). Propagation attenuation is subtracted from it, so lowering base volume during a lull will still produce the correct filtered-and-quieter result through doorways.
✅ Result: One knob → rain swells or fades globally, while all of the per-portal routing, blending, and filtering stays correct.
Troubleshooting
“Rain sounds full-volume indoors.”
- Check the portal’s
+Zpoints through the opening. If the portal’s forward is parallel to the wall, the blend axis is wrong. - Inspector error on the portal GameObject? Zone colliders must overlap the portal collider by ≥ 5 cm.
“I hear a pop when crossing a doorway.”
- Clip isn’t loop-authored. The emitter is set to
loop = true— Unity won’t fix a boundary click in the clip itself. Edit the clip, crossfade the seams.
“No sound at all — AudioZones are registered but silent.”
- Is the
Audio Listener Zone Trackeron your player object, the one with the Collider? On the camera that holds the AudioListener it gets no trigger events, so the listener’s zone is always null. - Confirm the
Ambience Mixer Groupisn’t muted or at -∞ dB. - Check the
activationRadiuson your source zone. Outdoor source zones should usually usefloat.PositiveInfinity.
“Opening the door doesn’t change anything.”
- Your
IPortalDoorSourceimplementation needs to fireOnChanged. Without it, portals poll via the solve tick (~67 ms max latency at the default 15 Hz); with it, solves run immediately on state change.
“The listener spawns inside a room and hears nothing until they walk out and back in.”
- Make sure the listener GameObject has a Rigidbody (the tracker auto-adds a kinematic one) and the zone collider’s
isTrigger = true(the zone’sOnValidateenforces this). - Propagation scans for listener membership on first frame — if that still misses, it’s likely a layer/collision-matrix issue.
From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.