Pepper.SFX
All Guidance pages

Guidance / API reference

Loading

Namespaces: AudioSystem.Loading (always available) · AudioSystem.Loading.Addressables (gated by com.unity.addressables)

The clip-loading layer: import policy, warming, and the optional Addressables backend. See the User Guide → Audio Loading & Memory for the concepts and setup.

Always available (SFXSystem.Runtime)

// The seam every container resolves clips through. Default = DirectClipSource.
interface IAudioClipSource {
    bool TryGetClip(in AudioClipRef clipRef, out AudioClip clip); // hot path; false = cold → drop
    bool SupportsAddressables { get; }   // false on Direct — tells a real misconfig from a cold-drop
    void Pin(string address);            // hold an addressable clip resident (persistent holders); no-op on Direct
    void Unpin(string address);
}

// Optional companion an addressable source also implements (Direct does not).
interface IVoiceLifecycleListener {
    void OnVoiceBound(AudioVoice voice);     // source.clip just assigned → voice refcount++
    void OnVoiceReleased(AudioVoice voice);  // voice returning to pool → voice refcount--
}

readonly struct AudioClipRef {
    AudioClip DirectClip; string Address;    // an entry is EITHER a direct clip OR an address
                                             // Address = the Addressables address (what a bank label resolves to),
                                             // not the asset GUID unless the address IS the GUID
    bool HasAddress; bool IsEmpty;
    AudioClipRef(AudioClip clip);
    AudioClipRef(AudioClip clip, string address);
}

static class AudioClipSources {
    static IAudioClipSource Active { get; set; }          // default DirectClipSource; assigning null reverts to it
    static IVoiceLifecycleListener VoiceListener { get; } // cached; the active source's listener, if any
    // Resolve + diagnose: warns once-per-address when an address can't resolve for lack of a backend
    // (vs an expected cold-drop). Containers / AmbientSource use this instead of Active.TryGetClip.
    static bool TryResolve(in AudioClipRef clipRef, Object context, out AudioClip clip);
}

// Pre-load clip audio data ahead of play (coroutines). No Addressables dependency.
static class AudioClipWarmer {
    static bool IsWarm(AudioClip);
    static IEnumerator Warm(AudioClip);
    static IEnumerator WarmAll(IEnumerable<AudioClip>);    // parallel
    static bool Unload(AudioClip);                         // blunt — no live-voice check (Addressables handles that)
}

// AudioManager convenience hosts (it's the persistent coroutine runner). Return the coroutine.
Coroutine AudioManager.WarmClip(AudioClip);
Coroutine AudioManager.WarmClips(IEnumerable<AudioClip>);
Coroutine AudioManager.WarmContainer(AudioContainer);      // uses EnumerateClips()

// Every clip a container could play (cycle-safe; recurses Switch/Blend children):
IEnumerable<AudioClip> AudioContainer.EnumerateClips();

// Optional addressable key per entry (empty for direct entries):
class WeightedAudioClip { AudioClip clip; float weight; float volumeMultiplier; string address; } // Random
class SequenceEntry     { AudioClip clip; …; string address; }                                    // Sequence
class RoutingEntry      { AudioClip clip; string address; }                                        // Routing

// AmbientSource: addressable loop clip — pins the clip on the active source for its active life
// (and moves the pin if the active source changes, e.g. the Addressables backend starting later).
string   AmbientSource.LoopingClipAddress;   // serialized field; Inspector: "Looping Clip Address"
AudioClip AmbientSource.PlayableClip;         // resolved clip; null while an addressable clip is loading

// Raised on genuine listener zone-membership change (the addressable bank binder subscribes).
// Always paired one-to-one: an Exit is also raised when ResyncListenerAfterTeleport leaves a zone,
// when a zone the listener is in is disabled/destroyed/unloaded, and when the manager itself is destroyed.
static event Action<AudioZone> PropagationManager.ListenerEnteredZone;
static event Action<AudioZone> PropagationManager.ListenerExitedZone;

Gated by com.unity.addressables

Namespace AudioSystem.Loading.Addressables; assembly SFXSystem.Addressables (compiles only when the package is present).

// Refcounted addressable clip source. Installed as AudioClipSources.Active when AddressableAudioRuntime starts the backend.
sealed class AddressableClipSource : IAudioClipSource, IVoiceLifecycleListener {
    float LingerSeconds;                    // keep-warm after refcount 0 (default 5)
    void BankAcquire(string address);       // called by AudioBank
    void BankRelease(string address);
    void Tick(float unscaledNow);           // linger sweep (driven by AddressableAudioRuntime)
    void ReleaseAll();                       // teardown hard-reset (truth is zero)
    void GetDebugEntries(List<DebugEntry>);  // overlay readout
}

// A bank = an Addressables label. Preload loads/refcounts every clip under it; Release drops them.
sealed class AudioBank {
    AudioBank(string label, AddressableClipSource source);
    string Label; bool IsLoaded;
    void Preload();   // idempotent (refcounted per bank; survives re-entered zones)
    void Release();
}

// MonoBehaviours (Add Component ▸ Audio ▸ Loading):
ZoneAudioBank            // on an AudioZone GameObject; holds the bank label (BankLabel)
AddressableAudioRuntime  // starts the backend: installs the source, binds zone events, ticks linger.
                         // The backend persists across scene loads (own hidden DontDestroyOnLoad object);
                         // one runtime is enough — later ones (other scenes) log and do nothing.

Refcount model: resident(address) = (banks holding it) + (live voices bound to it) + (pins) > 0. Bank Release, OnVoiceReleased, and Unpin each decrement; the real unload fires at 0, after LingerSeconds. A clip a bank or pin loads also has its audio data loaded, so its first play doesn’t hitch. Teardown (ReleaseAll) force-releases everything regardless of count; it runs only when the backend is destroyed (application quit / leaving Play Mode), not on scene loads.

Cold-post policy (v1): a not-yet-resident addressable clip makes TryGetClip return false → the play is dropped (like a null clip, but without the missing-clip warning). A Sequence container stays on a cold entry, so its next play tries that entry again; an Auto Advance run ends there. Bank preload is the warm path; there is no defer-and-play-late in v1.

Editor tooling: Window ▸ Audio System ▸ Addressable Audio Authoring (Convert/Revert containers; gated assembly SFXSystem.Addressables.Editor) and Window ▸ Audio System ▸ Loading Validator (always-on; runs via reflection even without the package).

From the Pepper.SFX 3.1 guides (API_REFERENCE.md). Something wrong or unclear? Tell me.