Guidance / API reference
Propagation
Namespace: AudioSystem.Propagation
Added in: v2.3.0
Location: Assets/Scripts/SFX_System/RunTime/Propagation/
Zone/portal graph-based routing for long-running ambient beds. Additive — does not modify the SFX event pipeline. See Manual Chapter 13 for architecture and design rationale.
PropagationManager
Namespace: AudioSystem.Propagation
Inheritance: MonoBehaviour
Access: PropagationManager.Instance (lazy-instantiated at play time)
Singleton orchestrator. Owns zone/portal/source registries, the Dijkstra solver, the emitter pool, and per-frame smoothing.
Properties
public static PropagationManager Instance { get; }
public static bool HasInstance { get; }
Instance returns null in Edit mode (to prevent phantom GameObject creation on inspector edits). HasInstance is null-safe for use in OnDisable during scene teardown.
Inspector Configuration
| Field | Default | Description |
|---|---|---|
solveRateHz |
15 | Solves per second. Per-frame emitter smoothing handles the in-between. |
distancePenaltyDbPerMeter |
0 | Extra dB cost per meter of portal-to-portal distance. 0 = pure transmission routing. |
silenceOutsideGraph |
false | When false, voices outside every AudioZone play at full audibility (whitelist mode). When true, they fall silent and maximally muffled (complete-coverage mode). See Manual §13.4. |
transmissionSkipThreshold |
1e-5 | Linear-amplitude floor below which the solver treats a portal as fully closed and skips it. Raise to cull near-closed doors earlier. |
drawListenerZoneOverlay |
false | Editor-only debug overlay. When ON, draws a scene-view label above the listener showing the current zone name and inside-portal count. |
maxAmbientEmitters |
16 | Global cap. Farthest emitters silenced first when exceeded. |
prewarmPoolSize (range 0–64) |
0 | Pre-allocate this many AmbientEmitter GameObjects at Start. Clamped to maxAmbientEmitters. 0 = lazy. |
emitterSmoothSpeed |
4 | Per-frame smoothing speed for volume/cutoff/position. |
maxPathsPerSource (range 1–6) |
1 | How many simultaneous propagation paths each ambient source may voice. 1 = original single-best-path. Raise to hear an ambience leak through multiple openings at once. |
enableObstruction |
false | Per-emitter listener→virtual-emitter raycast obstruction on top of portal-based propagation. |
obstructionLayerMask |
~0 | Physics layer mask for the obstruction raycast. |
obstructionAttenuationDb (0–30) |
6 | Extra dB applied when the obstruction ray is blocked. |
obstructionCutoffHz (100–22000) |
2000 | Upper bound on the low-pass cutoff under obstruction. Combines with solver cutoff via min-wins. |
zoneDwellSeconds (0–1) |
0.15 | Dwell time before a listener zone enter/exit commits. Absorbs straddle-jitter on boundary crossings. 0 = instant commit. |
culler |
— | Nested PropagationProximityCuller configuration (see below). |
listenerOverride |
null | Optional explicit listener transform. Without one, the listening point registered with Pepper (AudioManager.AddListeningPoint); only with no point, the scene’s AudioListener, then Camera.main. |
Registration API
public void RegisterZone(AudioZone z);
public void UnregisterZone(AudioZone z);
public void RegisterPortal(AudioPortal p);
public void UnregisterPortal(AudioPortal p);
public void RegisterSource(AmbientSource s);
public void UnregisterSource(AmbientSource s);
Zones, portals, and ambient sources self-register via their OnEnable / OnDisable. Manual calls are safe (idempotent).
Notification API (typically called only by AudioListenerZoneTracker)
public void NotifyListenerEnteredZone(AudioZone z);
public void NotifyListenerExitedZone(AudioZone z);
public void NotifyListenerEnteredPortal(AudioPortal p);
public void NotifyListenerExitedPortal(AudioPortal p);
public void NotifyPortalParamsChanged(AudioPortal p);
NotifyPortalParamsChanged triggers an immediate re-solve instead of waiting for the next scheduled tick. Called from AudioPortal.HandleDoorChanged when the door source fires OnChanged.
Queries
public AudioZone GetCurrentListenerZone();
public AudioZone GetZoneContainingPoint(Vector3 worldPoint);
public float GetSourceAudibility(AudioZone sourceZone);
public float GetSourcePropagationCutoff(AudioZone sourceZone);
public AudioZone GetResolvedListenerReverbProfile(Vector3 listenerPos, AudioReverbProfile dest);
public AudioZone GetResolvedListenerReverbProfile(AudioReverbProfile dest);
public bool SilenceOutsideGraph { get; }
GetCurrentListenerZone— top of the listener zone stack, or null if the listener isn’t inside any registered zone.GetZoneContainingPoint— first registered zone whoseContainsPoint(worldPoint)returns true, or null. Used by the reverb-send driver to resolve source zones.GetSourceAudibility/GetSourcePropagationCutoff— per-zone reads from the listener-audibility cache, populated each solve tick. Same-zone = 1.0 / 22 kHz; through-portals = chain-of-portals weight; unreachable = 0 / 500 Hz.GetResolvedListenerReverbProfile— fills the providedAudioReverbProfilewith the listener’s current zone’s reverb character, resolved zone →ReverbBus→ReverbSendBusSO (portal-blend lerped when inside a blend region). Returns the canonical zone that was resolved. Still used by the per-source ambient path (AmbientEmitter); the per-bus parametric driver no longer calls it.
AudioZone
Namespace: AudioSystem.Propagation
Inheritance: MonoBehaviour
Required Components: BoxCollider (auto-forced to isTrigger = true)
A volume of world space tagged as one acoustic room. Nodes of the propagation graph.
Inspector
| Field | Default | Description |
|---|---|---|
zoneId |
"" | Optional debug identifier. Not used at runtime. |
baseVolumeDb |
0 | Optional per-zone attenuation baseline added to ambient source volume. |
activationRadius |
50 | Distance from listener beyond which this zone is culled. Infinity = never cull. The culler preserves transitive reachability — a zone connected to the listener’s zone through near portals is force-included even when individually out of range. |
entryFadeMeters (Min 0) |
0 | Soft membership band measured inward from the trigger surface. 0 = hard boundary (binary in/out). When > 0, a point at the surface has membership 0 and a point this many meters inside has membership 1; lerps linearly in between. Used to fade reverb sends, listener-side reverb character, and BaseVolumeDb smoothly across the boundary. |
reverbBus |
null | The mixer reverb bus voices physically in this zone send to (source-side). Drag in the ReverbBus field from a ReverbSendBus asset. Null = no reverb-send routing for sources in this zone. The bus’s character lives on its ReverbSendBus asset, not here. See Reverb Send Buses. |
Properties
public string ZoneId { get; }
public float BaseVolumeDb { get; }
public float ActivationRadius { get; }
public float EntryFadeMeters { get; }
public AudioMixerGroup ReverbBus { get; } // null when no bus assigned; routing only (character lives on the ReverbSendBus SO)
public IReadOnlyList<BoxCollider> VolumeColliders { get; }
VolumeColliders is populated from GetComponents<BoxCollider>() in Awake — add multiple BoxColliders for L-shapes, they’re OR-unioned.
Virtual Methods (override for custom shapes)
public virtual bool ContainsPoint(Vector3 worldPoint);
public virtual Vector3 ClosestSurfacePoint(Vector3 worldPoint);
public virtual float GetMembershipFactor(Vector3 worldPoint); // 0..1; binary when entryFadeMeters == 0
Default implementations iterate all volume colliders. Subclass to support sphere-shaped or mesh-shaped zones without touching the solver. Subclasses that override GetMembershipFactor must respect the contract “0 at surface, 1 once fully inside.”
AudioPortal
Namespace: AudioSystem.Propagation
Inheritance: MonoBehaviour
Required Components: BoxCollider (auto-forced to isTrigger = true)
Edge of the propagation graph. Connects two zones, optionally modulated by an IPortalDoorSource, and doubles as the listener blend region.
Inspector — Zones
| Field | Default | Description |
|---|---|---|
zoneA |
null | One of the two connected zones. |
zoneB |
null | The other connected zone. |
autoDetectZones |
false | On first enable, probe the portal’s own trigger-collider world bounds and pick the two AudioZones whose volumes overlap. Lets designers drop a portal into a doorway without hand-assigning the zone refs. One-time scan at OnEnable; manually-assigned refs are kept as fallback if fewer than two are found. |
zoneA and zoneB are required (either assigned manually or filled by auto-detect). OnValidate errors if either is null or if zoneA == zoneB.
Inspector — Door
| Field | Description |
|---|---|
doorSourceRaw |
MonoBehaviour implementing IPortalDoorSource. Null = always open. |
OnValidate clears the reference and logs an error if the assigned MonoBehaviour doesn’t implement the interface.
Inspector — Transmission
| Field | Default | Description |
|---|---|---|
baseTransmission |
1.0 | Amplitude multiplier when fully open (OpenProgress = 1). |
closedTransmission |
0.1 | Amplitude multiplier when fully closed (OpenProgress = 0). |
openLowPassHz |
12000 | Low-pass cutoff when fully open. Slightly muffled but audibly distinct from transparent — a door-less portal at least makes itself heard. |
closedLowPassHz |
600 | Low-pass cutoff when fully closed. |
OnValidate warns if closedTransmission > baseTransmission or closedLowPassHz > openLowPassHz (values likely swapped).
Inspector — Blend Region
| Field | Default | Description |
|---|---|---|
transitionAxisLocal |
Vector3.forward |
Local-space axis pointing through the opening. Used for blend-factor computation. |
activationRadius |
50 | Distance from listener beyond which this portal is culled. |
Properties
public AudioZone ZoneA { get; }
public AudioZone ZoneB { get; }
public IPortalDoorSource DoorSource { get; }
public float ActivationRadius { get; }
public BoxCollider TriggerCollider { get; }
public event Action<AudioPortal> OnPortalParamsChanged;
Methods
public float GetOpenProgress(); // 0..1; returns 1 if no door source
public float GetTransmissionMultiplier(); // linear amplitude, dB-interpolated
public float GetCutoffHz(); // log-frequency-interpolated
public Vector3 GetVirtualEmitterPosition(Vector3 listenerWorldPos);
public float GetBlendFactor(Vector3 listenerWorldPos); // 0..1 along transition axis
public AudioZone GetZoneAtBlendFactorZero(); // zone on the blend=0 axis side
public AudioZone GetZoneAtBlendFactorOne(); // zone on the blend=1 axis side
public AudioZone GetOpposite(AudioZone z); // solver graph traversal
GetZoneAtBlendFactorZero/One auto-detect which zone sits on each side of the transition axis (lazy-cached on first call). Designers never configure this — they just orient the GameObject’s +Z through the opening.
AmbientSource
Namespace: AudioSystem.Propagation
Inheritance: MonoBehaviour
Declares an ambient bed. Does not play audio directly — the PropagationManager drives a pooled AmbientEmitter on its behalf.
Inspector — Source
| Field | Default | Description |
|---|---|---|
sourceZone |
null | The zone where this ambience originates (required). |
loopingClip |
null | The looping audio clip. Must be loop-authored (no pops at boundary). |
ambienceMixerGroup |
null | AudioMixerGroup to route through. Use the Ambience bus for bus ducking / mix fades. |
useOcclusion |
false | Flag for the Occlusion Layout Builder scan. When true, this source’s mixer group is included so a slot layer is generated under it. Does not change emitter audio behavior — propagation drives the per-emitter low-pass directly. |
Inspector — Level
| Field | Default | Description |
|---|---|---|
sourceBaseVolumeDb |
0 | Baseline volume in dB. Propagation attenuation is subtracted from this. Clamped to [-60, 12] in the inspector. |
playOnStart |
true | Auto-register with PropagationManager on enable. Set false for sources controlled manually (weather toggling rain). |
Inspector — 3D Rolloff (optional override)
| Field | Default | Description |
|---|---|---|
overrideRolloff |
false | Master switch for the override block. When false, the emitter uses neutral defaults (Linear / 1m / 500m), tuned for ambient beds whose spatial position is dominated by the propagation solver. |
rolloffMode |
Linear |
Distance falloff curve. Linear = evenly distributed; Logarithmic = matches Unity’s default. Custom not supported here. |
minDistance (Min 0.01) |
1 | Distance at which the source is at full volume. |
maxDistance (Min 0.1) |
500 | Distance at which the source becomes inaudible (Linear) or quiet (Log). |
Override settings apply to the AudioSource component on the managed emitter each time it’s acquired from the pool. Leave overrideRolloff = false (the default) when the source’s role is “propagation-routed bed” — the solver’s per-emitter LPF and the portal-driven virtual-emitter position handle spatialisation. Turn on for tight point-source ambiences (a humming machine that needs a 10m maxDistance, etc.).
Properties
public AudioZone SourceZone { get; }
public AudioClip LoopingClip { get; }
public AudioMixerGroup MixerGroup { get; }
public float SourceBaseVolumeDb { get; }
public bool UseOcclusion { get; }
Methods
public void SetBaseVolumeDb(float db); // clamped to [-80, 20]; picked up on next solve
public void Register(); // manual registration (when playOnStart is false)
public void Unregister(); // fades out cleanly, doesn't abruptly stop
public void ApplyRolloffTo(AudioSource s); // applies override (or defaults) to the emitter's AudioSource;
// called by AmbientEmitter.Initialize on acquire
AmbientEmitter
Namespace: AudioSystem.Propagation
Inheritance: MonoBehaviour (sealed)
Menu: Hidden from Add Component menu — managed programmatically by PropagationManager
Pooled persistent looping voice. Owns its own AudioSource and AudioLowPassFilter. Bypasses the SFX voice pool by design — routed through the designated Ambience mixer group for bus integration.
You generally don’t interact with this component directly; the PropagationManager pool manages lifecycle.
Properties
public AmbientSource OwningSource { get; }
public AudioPortal OwningPortal { get; } // null for direct same-zone playback
public bool IsSilent { get; } // safe-to-recycle check
public float TargetVolumeLinear { get; }
public float CurrentVolumeLinear { get; }
public Vector3 TargetPosition { get; }
Methods (called by PropagationManager)
public void Initialize(AmbientSource owner, AudioClip clip, AudioMixerGroup group, float smoothSpeed);
public void FadeInTo(AudioPortal portal, float targetVolLinear, float targetCutoffHz, Vector3 pos);
public void SetTarget(float volumeLinear, float cutoffHz, Vector3 pos);
public void SetOwningPortal(AudioPortal portal);
public void FadeOut();
public bool Tick(float dt); // returns true when safe to release
public void ReleaseImmediate(); // unconditional stop + state reset
AudioListenerZoneTracker
Namespace: AudioSystem.Propagation
Inheritance: MonoBehaviour
Attach to the GameObject that carries the player’s Collider, usually the player root, not the camera that holds the AudioListener: Unity only sends trigger events to a GameObject with a Collider. Watches for trigger enter/exit on AudioZone and AudioPortal colliders and forwards events to PropagationManager.
Auto-adds a kinematic Rigidbody if none is present — required for Unity trigger events on static zone/portal colliders.
Inspector
| Field | Default | Description |
|---|---|---|
audioZoneLayer |
Everything | Optional layer mask filter. Limits trigger detection to specified layers. |
IPortalDoorSource
Namespace: AudioSystem.Propagation
Type: Interface
Minimal contract any MonoBehaviour can implement to drive an AudioPortal’s open/closed state. Portable — propagation does not depend on any project-specific door system.
public interface IPortalDoorSource
{
float OpenProgress { get; } // 0 = closed, 1 = open
event System.Action OnChanged; // fire when OpenProgress changes meaningfully
}
Firing OnChanged triggers an immediate re-solve on the subscribed portal. Spamming it every frame is safe but redundant; prefer threshold-based dispatch (e.g. Δ ≥ 0.02).
Adapter Pattern
Projects typically bridge their own door state (animator float, ScriptableObject channel, physics hinge) with a thin adapter MonoBehaviour:
using AudioSystem.Propagation;
public class DoorAnimatorAdapter : MonoBehaviour, IPortalDoorSource
{
public float OpenProgress => animator.GetFloat("openAmount");
public event Action OnChanged;
// ... fire OnChanged on threshold changes
}
PropagationProximityCuller
Namespace: AudioSystem.Propagation
Type: [Serializable] class (sub-component of PropagationManager, not a separate MonoBehaviour)
Trims the active solver graph to zones and portals within range of the listener. Scales propagation to large levels without modifying solver logic.
Inspector (nested under PropagationManager)
| Field | Default | Description |
|---|---|---|
cullCheckFrameInterval |
10 | Frames between active-set recomputations. 0 = culler disabled (everything active). |
globalMaxActiveZones |
64 | Hard cap on active zones after radius filter. Farthest dropped first. |
globalMaxActivePortals |
128 | Hard cap on active portals after radius filter. |
Properties
public bool Enabled { get; } // true if cullCheckFrameInterval > 0
The culler’s first tick runs immediately regardless of interval, so the solver never sees an empty graph during startup.
From the Pepper.SFX 3.1 guides (API_REFERENCE.md). Something wrong or unclear? Tell me.