Pepper.SFX
All Guidance pages

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 whose ContainsPoint(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 provided AudioReverbProfile with the listener’s current zone’s reverb character, resolved zone → ReverbBus → ReverbSendBus SO (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.