Pepper.SFX
All Guidance pages

Guidance / Manual

Performance & Optimization

9.1 Voice Management

Voice Limits

Default Configuration:

maxRealVoices = 32;      // Physically playing sounds
maxVirtualVoices = 64;   // Virtualized sounds (time-tracking only)

What Happens When Limit Reached:

  1. Over maxRealVoices, or over a bus’s own Max Real Voices (§6.9): the least important plays go virtual (a play is a whole sound, all of an engine’s layers), keeping their place in the clip (§3.2, Voice Virtualization).
  2. Over Real + Virtual: an event makes room for its whole new play by ending whole plays, silent virtual ones first, then the least important. Persistent ones (§5.3) are never ended. If there isn’t room even then, the new sound is skipped and nothing is stolen. A stolen sound fades out over Steal Fade Seconds (§3.2). VoiceStealBehavior only applies to an event’s own Max Instances.
  3. Critical priority makes a sound the last to go, not exempt: only Persistent plays are never stolen

Unity’s own limits. Unity has its own Max Real Voices and Max Virtual Voices (Project Settings > Audio; 32 and 512 by default). If Pepper’s Max Real Voices is higher than Unity’s, or Real + Virtual + Steal Headroom is higher than Unity’s virtual count, Pepper warns at startup and the Audio Manager inspector shows the numbers side by side. Over its own real limit, Unity silences sources itself, behind Pepper’s back; Pepper sets AudioSource.priority from each event’s priority (persistent sounds highest) so Unity spares the important ones.

Recommended Settings:

Desktop/Console: 32-64 real voices
Mobile: 16-24 real voices
VR: 24-32 real voices (high frame rate priority)

Voice Stealing Strategies

By Priority:

public enum VoicePriority
{
    Low = 0,       // Weighs 1/8: distant ambience
    Medium = 64,   // Weighs 1/4: standard gameplay
    High = 128,    // Weighs 1/2: player actions
    Critical = 255 // Weighs 1, the strongest (still stealable; Persistent is the exemption): UI, dialogue
}

A sound’s importance is how loud it is where it’s heard times its priority’s weight (§3.2), so a near, loud Low sound can outrank a far, quiet Medium one.

By Behavior:

public enum VoiceStealBehavior
{
    Oldest,          // Steal longest-playing voice
    Quietest,        // Steal quietest voice
    Furthest,        // Steal most distant voice
    LowestPriority   // Steal by priority only
}

Best Practice:

UI sounds: Critical priority
Player footsteps: High priority
Enemy footsteps: Medium priority
Distant ambience: Low priority

Voice Virtualization

What Gets Virtualized:

  1. Voices beyond the maxRealVoices limit, or a bus’s Max Real Voices, least important first (distance × volume × the priority’s weight)
  2. Sounds past their Attenuation asset’s Max Distance (in Spatial Mode = Pepper, from every listening point)
  3. Nothing else: there is no other distance threshold, and under the limits every voice plays

How It Works:

// Becoming virtual (the voice budget does this itself; you can too)
voice.MakeVirtual();
// - Fades out over about 25 ms if it can be heard, then pauses the AudioSource
// - Keeps its place in the clip, in samples, at its pitch
// - No audio output, minimal CPU cost
// - Called from your code: it stays virtual until MakeReal(); the budget won't bring it back

// Becoming real again
voice.MakeReal();
// - Moves the AudioSource to where it would have been, then resumes it
// - Fades back in over about 25 ms
// - Handles looping correctly

A voice comes back in phase at any pitch, Pepper’s Doppler included, except for Unity’s own Doppler, which Pepper can’t see: give looping layers that must stay in step Doppler = Pepper, or a Doppler Level of 0 (§3.2, Voice Virtualization).

Monitoring:

void OnGUI()
{
    var stats = AudioManager.Instance.GetStatistics();
    GUI.Label(new Rect(10, 10, 400, 20),
        $"Real: {stats.activeVoices}/{stats.realVoiceBudget}, " +
        $"Free in the pool: {stats.availableVoices} of {stats.poolSize}");
}

9.2 Memory Optimization

AudioClip Import Settings

UI Sounds (Small, Frequent):

Load Type: Decompress On Load
Compression Format: PCM
Quality: 100%

Reason: Small file size, instant playback, minimal CPU

SFX (Medium, Common):

Load Type: Compressed In Memory
Compression Format: Vorbis (mobile) or ADPCM (desktop)
Quality: 70-90%

Reason: Balance of file size, memory, and CPU

Music (Large, Streaming):

Load Type: Streaming
Compression Format: Vorbis
Quality: 70-80%

Reason: Large files stream from disk, low memory cost

Ambience (Large Loops):

Load Type: Streaming
Compression Format: Vorbis
Quality: 60-70%

Reason: Long loops, stream from disk

Resource Loading Strategy

Startup (Automatic):

// AudioManager.Awake()
var audioEvents = Resources.LoadAll<AudioEvent>("Audio/Events");
var audioStates = Resources.LoadAll("Audio/States", typeof(AudioState));

Key Insight: Events and States load once at startup. Containers and clips are referenced, not duplicated.

Memory Footprint:

Events: Minimal (ScriptableObject metadata)
States: Minimal (ScriptableObject metadata)
Containers: Minimal (references to clips)
Clips: Depends on import settings
Voices: Fixed pool of Max Real + Max Virtual + Steal Headroom AudioSources (100 by default), built at start

9.3 CPU Optimization

Update Intervals

Voice Management:

voiceUpdateInterval = 0.1f; // 10 times per second

// What it does:
// - Check if voices finished
// - Update virtualization
// - Clean up inactive voices

Tuning:

  • Faster (0.05s): More responsive, higher CPU
  • Slower (0.2s): Lower CPU, less responsive
  • Recommended: 0.1s for most games

Occlusion:

occlusionUpdateInterval = 0.2f; // 5 times per second

// What it does:
// - Raycast from listener to each voice
// - Update occlusion gain
// - Update low-pass filter

Tuning:

  • Faster (0.1s): Smooth occlusion, high CPU
  • Slower (0.3-0.5s): Lower CPU, slight lag
  • Recommended: 0.2s for most games, 0.3-0.5s for mobile

Occlusion Optimization

Layer Mask:

// Include only large geometry
occlusionMask = LayerMask.GetMask("Environment", "Buildings");

// Exclude:
// - Small props
// - Moving objects
// - Player
// - Enemies

Per-Voice Occlusion:

Only enable for important 3D sounds
Disable for:
  - 2D sounds (UI, music)
  - Distant ambience
  - Non-critical sounds

Distance and Virtualization

There are no fixed distance tiers. The lodDistances field on the AudioManager is not used in this version and is hidden in the inspector. Distance matters only through a voice’s importance score: for a 3D sound it falls from 1 at the listener to 0 at the sound’s own Max Distance (with an Attenuation asset, its curve’s level there; with Spatial Mode = Pepper, the level it is heard at; 2D sounds don’t lose importance with distance). When more than maxRealVoices are live, the lowest scores are virtualized first, so distant sounds give way before nearby ones. While you are under the limit, every sound plays regardless of distance.

To make a sound more or less likely to be virtualized, change its event’s Priority or its container’s Max Distance.

Listening Points

Everything in Pepper that depends on where the player is listening (voice importance, occlusion rays, the Furthest steal, which multi-position emitter is nearest, and with Spatial Mode = Pepper each 3D sound’s level and pan) reads the listening points.

Register one (recommended). Tell Pepper where you listen from, usually the camera, and it never searches the scene:

// Follow a transform: the point reads its pose every frame.
AudioListeningPoint point = AudioManager.Instance.AddListeningPoint(cameraTransform);

// Or move it yourself, in Update or LateUpdate:
AudioListeningPoint point = AudioManager.Instance.AddListeningPoint();
point.SetPose(rigPosition, rigRotation);

// When that camera goes away:
AudioManager.Instance.RemoveListeningPoint(point);
  • Pepper’s per-frame step runs after every LateUpdate, so a camera rig that moves in LateUpdate (Cinemachine) is already in place.
  • The point’s velocity (for Approach Speed and Doppler) is measured from how it moves each frame, unless you give it with SetVelocity. ClearVelocity() goes back to measuring, starting again from where the point is: call it on a camera cut too, so the cut isn’t read as speed. Pointing Follow at another camera does that by itself.
  • If the transform a point follows is destroyed, the point is removed (IsRegistered turns false), with a warning once a session. Remove your points when their camera goes; the warning says you didn’t. (It used to stay where it last saw the camera, still in use.)
  • Unity’s AudioListener still does Unity’s own 3D panning. If it isn’t on the transform the point follows, AudioManager.Instance.DriveUnityListener(listener) moves it to the primary point every frame, so both hear from the same place.

The primary point. With several points, one is primary: the one DriveUnityListener keeps Unity’s AudioListener on, the one the ambient system (§13) and SingleEmitter posts listen from, and with Spatial Mode = Unity the only one Pepper listens from. It is the first registered until you pick one:

AudioManager.Instance.SetPrimaryListeningPoint(carViewPoint);   // menu camera → car view
bool isIt = carViewPoint.IsPrimary;
AudioManager.Instance.SetPrimaryListeningPoint(null);           // back to the first registered

Changing it doesn’t make sounds jump: what Pepper measures from a point (Distance, Approach Speed and Angle, so Doppler and every curve on them, and attenuation filters) glides to the new one over about 100 ms. With Spatial Mode = Unity, Unity’s own levels and pans still jump when its AudioListener moves.

Several points, weighted: Spatial Mode = Pepper. One AudioListener can’t hear two places, so for split screen Pepper pans and attenuates 3D sounds itself. Set the Audio Manager’s Spatial Mode to Pepper (in the inspector, or AudioManager.Instance.SpatialMode = AudioSpatialMode.Pepper; before the sounds start: a change applies to sounds started after it). Then every 3D sound plays at Spatial Blend 0 and, each frame, Pepper sets its level and pan from every point:

  • Level: for each point, the sound’s attenuation there (its Attenuation asset’s volume curve, else Unity’s own rolloff for its Min and Max Distance) times the point’s Weight (0–1). The loudest wins. So a sound is as loud as from the point that hears it best.
  • Pan: where it is, left to right, from each point, blended by how loud each point hears it, and narrowed by each point’s Pan Width (0 = mono, centred; 1 = full stereo). It eases over about 50 ms.
  • At one point of weight 1 and width 1 it sounds exactly as Unity’s 3D does (the same rolloffs, the same constant-power pan, Spread the same way; measured on Unity 6000.0 and 6000.3). So single player can run Pepper mode too, and hear the code split screen runs.
  • Which point a sound is measured from (for Distance, Approach Speed, Angle, Doppler, attenuation filters, occlusion rays and the Furthest steal): the one that hears it loudest, changing only when another hears it 1 dB louder, so a car halfway between two players doesn’t flip. When it changes, those values glide over about 100 ms.
  • Max Distance: a sound goes virtual only when it is past its Attenuation asset’s Max Distance from every point.
  • Importance (§3.2) is how loud it is heard, so a car near either player counts as near.
  • Doppler is always Pepper’s (§4.1).
  • After the points change (one added or removed, the primary, a Follow, Weight or Pan Width), levels move to the new ones over about 100 ms instead of jumping.
// Local split: both players' cameras, full weight, mono (top/bottom halves have no left and right).
var p1 = AudioManager.Instance.AddListeningPoint(player1Camera);
var p2 = AudioManager.Instance.AddListeningPoint(player2Camera);
p1.PanWidth = p2.PanWidth = 0f;

// Remote split: your camera in stereo, the second view's faintly (about −9 dB) and mono.
var mine = AudioManager.Instance.AddListeningPoint(myCamera);
var second = AudioManager.Instance.AddListeningPoint(secondViewCamera);
second.Weight = 0.35f;
second.PanWidth = 0f;

Pepper mode’s limits: no spatializer plugins (HRTF); the project’s Doppler Factor isn’t read; a stereo clip keeps its two channels, balanced by the pan, where Unity’s 3D folds it to mono, so give 3D sounds mono clips; up and down count only by shortening the sideways part. AudioSources that aren’t Pepper’s (the ambient system’s) are still Unity’s, heard from the AudioListener: keep it on the primary point with DriveUnityListener. The Live View (§10.6) shows each voice’s point, level and pan.

With no point registered, Pepper searches the scene for an AudioListener, as before, and logs a warning once: “No listening point is registered”. A listener it found is kept until it is destroyed. While there is none, it searches at most once a frame (it used to search on every call).

ListenerUtil.Set still works: it registers a point that follows the transform you give it. A point you add with AddListeningPoint takes priority over it. ListenerUtil.Invalidate() removes it and makes the next lookup search again. If its transform is destroyed, the search takes over, as before.

ListenerUtil.Set(newCamera.transform);   // older code: still fine
Vector3 where = ListenerUtil.GetPosition();

ListenerUtil.Get() returns the primary point’s transform. A point moved with SetPose has none, so use ListenerUtil.GetPosition() for the position.

In Play mode, the Audio Manager’s inspector lists the points (weight, pan width, which is primary), or warns when there are none.

9.4 Mobile Optimization

Recommended Settings:

maxRealVoices = 16;               // Lower limit
maxVirtualVoices = 32;            // Lower limit
voiceUpdateInterval = 0.15f;      // Slower updates
occlusionUpdateInterval = 0.4f;   // Much slower

Clip Settings:

All clips: Compressed In Memory or Streaming
Compression: Vorbis
Quality: 60-70% (lower for mobile)

3D Audio:

Use sparingly
Prefer 2D when possible
Limit max distance to reduce spatialization cost

Best Practices:

✓ Use instance limiting aggressively
✓ Lower voice counts
✓ Increase update intervals
✓ Disable occlusion or use very slow interval
✓ Profile on target device

9.5 Profiling

Unity Profiler

Audio Module:

Shows:
- Active AudioSource count
- AudioClip memory usage
- DSP buffer usage
- Audio thread time

What to Look For:

High voice count → Increase instance limits
High audio memory → Check clip import settings
Audio thread spikes → Reduce update frequencies

Custom Debug Tools

Voice Count Display:

public class AudioDebugUI : MonoBehaviour
{
    void OnGUI()
    {
        var stats = AudioManager.Instance.GetStatistics();
        int y = 10;

        GUI.Label(new Rect(10, y, 400, 20),
            $"Real Voices: {stats.activeVoices}/{stats.totalVoices}");
        y += 20;

        GUI.Label(new Rect(10, y, 400, 20),
            $"Free Voices: {stats.availableVoices}");
    }
}

Event Logging:

AudioHandle handle = myEvent.Post();
handle.OnStarted += () => Debug.Log($"Started: {handle.EventName}");
handle.OnFinished += () => Debug.Log($"Finished: {handle.EventName}");

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