Pepper.SFX
All Guidance pages

Guidance / API reference

Container Types

RoutingContainer

Menu: Create > Audio System > Routing Container

Properties

public List<AudioClip> AudioClips { get; }
public float Volume { get; }
public bool Loop { get; }

Description: Plays all assigned clips simultaneously. Use for layered sounds (e.g., explosion = blast + debris + shockwave).

Factory Method (Editor Only):

public static RoutingContainer CreateNew(string name, AudioClip clip, float vol = 1f, bool looping = false, bool is3D = false)

RandomContainer

Menu: Create > Audio System > Random Container

Properties

public List<WeightedAudioClip> AudioClips { get; }
public int AvoidRepeatLast { get; }
public bool UseWeighting { get; }
public float Volume { get; }
public bool Loop { get; }

Methods

public void ClearHistory()

Description: Clears the repeat-avoidance history: the shared one and every emitter’s.

Avoid Repeat Last keeps one history per AudioEmitter the container is posted on, so a car never repeats its own last clips whatever other cars played, and one shared history for posts without one. A networked post (inside an AudioRandomScope) ignores and doesn’t record it. Picks draw from Pepper’s random numbers (see AudioManager.RandomNumbers) and allocate nothing. At or above the clip count it does nothing (every clip stays available); OnValidate warns about it from two clips up.

Description: Randomly selects one clip per play, with weighting and repeat avoidance.


SequenceContainer

Menu: Create > Audio System > Sequence Container

Properties

public List<SequenceEntry> Entries { get; }
public SequenceContainer.PlaybackMode SequencePlaybackMode { get; }
public bool LoopSequence { get; }
public bool AutoAdvance { get; set; }
public float Volume { get; }

PlaybackMode (Nested Enum)

public enum PlaybackMode
{
    Forward,   // 0->1->2->3->0...
    Reverse,   // 3->2->1->0->3...
    PingPong,  // 0->1->2->3->2->1->0...
    Random     // Random order
}

Methods

public void ResetSequence()
public void SetIndex(int index)
public AudioVoice PlayNext(Vector3 position = default, GameObject parent = null)

Description: Plays clips in sequence. PlayNext() advances the sequence and plays the next clip. AutoAdvance controls whether the sequence auto-advances after each clip finishes.


SwitchContainer

Menu: Create > Audio System > Switch Container

Properties

public AudioSwitchGroup SwitchGroup { get; }       // the group asset, or null (then switchGroupName)
public string SwitchGroupName { get; }             // the asset's GroupName, or the name field
public List<SwitchEntry> SwitchEntries { get; }
public AudioContainer DefaultContainer { get; }
public SwitchChangeMode WhenSwitchChanges { get; } // NextPlayOnly (default), Crossfade, Restart
public float CrossfadeTime { get; }                // Crossfade only, seconds (unscaled); default 0.15
public float Volume { get; }

Methods

public void SetSwitch(string switchValue)          // sets the group's GLOBAL value (also before the first play)
public string GetCurrentSwitch()                   // the value the last play of this asset picked
public AudioContainer GetActiveContainer()         // the container the last play of this asset picked

Description: Selects a child container by the switch group’s value in the play’s scopes (handle, emitter, global, the group’s Default). The child plays as part of the same play, under its own node in the play’s tree: a Blend under it reads its parameter in that play’s scopes, and a Sequence under it reports later clips to the same handle. Every voice the child starts, later ones included, gets the Switch’s Volume.

When the value changes while it plays (WhenSwitchChanges): NextPlayOnly keeps the choice (the next play picks the new value). Crossfade and Restart put the play in the per-frame pass, which reads the value every frame. On a change, the new choice plays from its start under a new node of the same play, as late voices of the handle (no second OnStarted); with Crossfade both fade along equal-power curves in GainStack.SwitchGain, with Restart the old one fades out as a steal does (the Audio Manager’s Steal Fade Seconds, 0.025 s by default and at least three frames; 0 cuts it a few ms ahead) and the new one starts at full level. A choice still fading out that is chosen again fades back up. A choice faded to silence stops two DSP buffers later, and nothing more starts in it (a Sequence there stands down). Room is made for the new voices without stealing this play; if the pool can’t, the old voices stop at once and the new ones take them. A virtual play’s new voices start virtual. A paused play doesn’t follow until resumed. A value with nothing to play fades the play to silence and holds it alive. A Switch only follows if its first choice played something.


BlendContainer

Menu: Create > Audio System > Blend Container Implements: IRTPCListener

Properties

public List<BlendEntry> BlendEntries { get; }
public AudioParameter BlendParameter { get; }   // wins over the name when set
public string BlendParameterName { get; }       // the asset's ParameterName when one is set
public float Volume { get; }
public BlendLoopMode LoopMode { get; set; }           // FromLayers (default), ForceLoop, ForceOneShot
public SilentLayerBehaviour SilentLayers { get; set; } // KeepPlaying (default) or Virtualize
public float SilentThresholdDb { get; set; }          // Virtualize: a layer below this goes virtual (-60; -90..-20)
public const float SilentHysteresisDb = 6f;           // ...and comes back this far above it
[Obsolete] public bool Loop { get; }                  // never read; see LoopMode
public BlendLayout Layout { get; set; }               // VolumeCurves (default) or Regions
public CrossfadeShape Crossfade { get; set; }         // Regions: Linear, EqualPower (default), SCurve
public AudioParameter PitchFollowParameter { get; set; } // each layer plays at value ÷ its rootValue
public float AutoLayoutFade { get; }                  // share of each gap between roots a crossfade covers
public float TuningWarningSemitones { get; }          // the inspector's tuning check threshold
public IReadOnlyList<AudioVoice> LayerVoices { get; }

Methods

public float GetCurrentBlendValue()             // what a play with no emitter or handle value reads
public void AutoLayout()                        // regions from the entries' rootValue (Regions layout)
[Obsolete] public void UpdateBlend(float blendValue)   // sets the parameter's global value
public void OnRTPCChanged(string parameter, float value)

Layouts. VolumeCurves: each entry’s volumeCurve sets its level (0–1) against the Blend’s parameter, as before. Regions: each entry plays between regionMin and regionMax, fading in over fadeInWidth and out over fadeOutWidth in the Crossfade shape; AutoLayout places the regions half way between neighbouring entries’ rootValues, each crossfade covering AutoLayoutFade of the gap, the lowest reaching the parameter’s Min and the highest its Max. Equal Power keeps the summed power at 1 (unrelated recordings); Linear and S-Curve keep the summed level at 1.

Pitch follow. With PitchFollowParameter set, each entry with followPitch and a rootValue plays at value ÷ rootValue, clamped to ±followRangeSemitones, in the pitch stack’s Follow slot. Entries’ curves apply to that layer only.

Loop Mode. FromLayers: each layer loops as its own container says (as before). ForceLoop / ForceOneShot: every layer’s voices loop / don’t, later voices included. A nested Blend left at FromLayers takes the outer one’s. The old loop field is hidden and never read; Sound Builder (Simple) writes LoopMode (Loop ticked = ForceLoop, unticked = FromLayers).

Silent layers. Virtualize: each frame, a layer voice whose own level (BaseGain × MultiplierGain × RtpcGain × CurveGain: its volume, the crossfade and curves; not bus, occlusion, fades or crossfades) is below SilentThresholdDb is made virtual and held so while it stays below SilentThresholdDb + SilentHysteresisDb; it keeps its place in the clip and comes back in step. A layer silent at the start starts virtual. It is never stolen on its own: steals take whole plays. A nested Blend left at KeepPlaying takes the outer one’s.

One start time. Every voice of a play is scheduled on the play’s start time: two DSP buffers ahead for a play of several voices (one for a single voice), or the post’s explicit DSP time.

Description: Plays multiple containers, each layer at a level set by its curve against one parameter. Each play reads the parameter in its own scopes (handle, emitter, global, default), so two copies posted on two emitters blend independently. The layers follow the value once a frame, in Pepper’s per-frame step (see AudioManager.UpdateMode). With a BlendParameter asset, the curves’ X axis is in its unit and values are clamped to its range; with only a name, values are clamped to 0–1. Any container can be a layer, Blends and Switches included: each nested Blend reads its own parameter in the same play’s scopes, and a layer under several Blends gets the product of their curves. Voices a layer starts later (a Sequence’s next clip) get the curves and the Blend’s Volume too. Only plays with a Blend in their tree are evaluated each frame.

The Blend no longer registers as an RTPC listener. OnRTPCChanged is kept for code that calls it: with the Blend’s parameter name it sets that parameter’s global value. UpdateBlend is obsolete and does the same.


GeneratorContainer (Unity 6000.3 and later)

Namespace: AudioSystem.Generators · Assembly: SFXSystem.Generators, which compiles only on Unity 6000.3 and later (its SFXSYSTEM_GENERATORS define comes from the editor version). On earlier versions nothing here exists. Implements: UnityEngine.Audio.IAudioGenerator

An abstract container whose sound is computed while it plays instead of read from a clip: the container is itself the Unity 6.3 generator its voice’s AudioSource plays. Pepper’s engine generators (the granular engine and the additive layer, in development) derive from it; so can your own.

public float Volume { get; }
public abstract int ParameterCount { get; }                 // at most GeneratorValues.Capacity (8)
public abstract AudioParameter GetParameter(int index);     // value i of what each voice is sent; null reads 0
protected abstract GeneratorInstance CreateGenerator(ControlContext context, AudioFormat? nestedFormat,
                                                     ProcessorInstance.CreationParameters creationParameters);

How it plays. A voice from Pepper’s pool, like any container, so buses, volume, the pitch stack, Doppler, Pepper’s panning, the voice filters, stealing, pause, virtual voices and Blend nesting all apply. The AudioSource’s loop is on: it plays until it is stopped (EstimateDuration reports it as a loop). It reports isRealtime false, so the AudioSource’s pitch resamples it. A virtual voice is paused: it isn’t processed, and it carries on from where it was. When the voice goes back to the pool its generator is cleared.

Values. Every step, after the plays are evaluated, each playing voice is sent a GeneratorValues message: GetParameter(i) read in that voice’s own play (its handle, then its emitter, then the global value, then the default; after smoothing; clamped; a built-in from the play’s place). Two cars on one engine asset each get their own RPM. A paused voice is sent nothing until it resumes. A value is heard about one DSP buffer after it is sent. Each message also carries Seconds, the unscaled time of the step that sent it.

Writing a generator. Allocate an unmanaged real-time struct with Pepper’s control half:

protected override GeneratorInstance CreateGenerator(ControlContext context, AudioFormat? nestedFormat,
                                                     ProcessorInstance.CreationParameters creationParameters)
    => context.AllocateGenerator(new MyRealtime(), new GeneratorControl<MyRealtime>(), nestedFormat, creationParameters);
  • IGeneratorRealtime (extends Unity’s GeneratorInstance.IRealtime): Configure(int sampleRate) before the first buffer, Dispose() when the instance goes, plus Unity’s Process (fill the buffer, mono) and Update. In Update, GeneratorValues.Receive(context, pipe, ref latest, out float seconds) returns true when new values have arrived, with the time all the steps that sent them covered (a buffer often brings two frames’ values, or none). Move from the old values to the new ones over that time, plus what was left of the last move, not across one buffer: frames and buffers don’t line up, so a ramp per buffer moves a value the game changes every frame in uneven steps. Pepper’s engines do this. Allocate nothing managed on this side.
  • GeneratorControl<TRealtime>: sets the output to mono at the system’s rate and passes each GeneratorValues straight down to the real-time half.
  • GeneratorValues: Count, an indexer, Capacity = 8, Seconds (the step’s unscaled time; after 3.1.0-dev.11), and Receive (with or without seconds).

GrainTable

One engine sweep cut into grains, one engine cycle (two turns, 120 ÷ RPM seconds) each, for the granular player. Made by the Engine Grains window, not by hand. It keeps a reference to the sweep clip (which must be Decompress On Load) and, per grain, where it starts and how long it is (frames at the clip’s rate) and the RPM its length means.

public AudioClip Sweep { get; }
public IReadOnlyList<GrainTable.Grain> Grains { get; }   // Start, Length, Rpm; in the sweep's order
public int PeriodsPerCycle { get; }                       // periods of the fundamental in one cycle
public float MinRpm { get; }
public float MaxRpm { get; }
public GrainTable.RpmSource Source { get; }               // Known (typed) or FromPitch
public float FromRpm { get; }
public float ToRpm { get; }
public GrainTable.RpmCurve Curve { get; }                 // Known: StraightLine or EvenPitch
public int Cylinders { get; }

RpmCurve is how a typed RPM moves between From and To: StraightLine (the same RPM every second) or EvenPitch (the same number of semitones every second, as most synthesisers sweep: the RPM multiplies by the same factor).

GranularEngine

Menu: Create > Audio System > Granular Engine (Unity 6.3+) · Extends: GeneratorContainer

A car engine played grain by grain from Grain Tables. At the end of each grain it takes the grain whose RPM is nearest the car’s, picked at random among up to Variation grains either side that are within half a semitone (never the same one twice in a row), and resamples it to land exactly on the RPM, sample by sample by the RPM the sweep had there (a line through the grain’s RPM at its middle, at the pace its neighbours climb), so a held RPM holds its pitch through the grain. The RPM and load move to each step’s values over that step’s time. Each grain fades into the next over Crossfade Ms with gains that sum to 1 (the grains are cut in phase). The on-load and off-load sweeps blend by load at equal power; the idle loop takes over below the sweeps’ lowest RPM, at equal power from Idle Rpm. The AudioSource’s pitch (Doppler, pitch offsets) applies on top.

public AudioParameter Rpm { get; }        // value 0: the car's RPM
public AudioParameter Load { get; }       // value 1: 0 off load to 1 on load; null = on load only
public GrainTable OnLoad { get; }         // the climbing sweep
public GrainTable OffLoad { get; }        // optional: the falling sweep
public AudioClip Idle { get; }            // optional: an idle loop (Decompress On Load)
public float IdleRpm { get; }             // the RPM the idle loop was recorded at (900)
public float CrossfadeMs { get; }         // 1–10, default 3
public int Variation { get; }             // 0–8, default 2

The sweeps’ and idle’s samples are loaded into memory when a voice starts and stay loaded while the engine asset is; they are read again after a new analysis. With no Grain Table (and no idle) a post plays nothing and warns once.

AdditiveEngine

Menu: Create > Audio System > Additive Engine (Unity 6.3+) · Extends: GeneratorContainer

An engine’s firing orders as sine tones, for weight under a loop engine or a granular one (as a layer in a Blend); not a whole engine. Order 1 is the firing frequency, RPM × Cylinders ÷ 120. Each order’s level moves from its off-load to its on-load level with the load. A little noise, low-passed at Noise Brightness times the firing frequency, takes the edge off. Orders at 45% of the output’s sample rate or above are left out; at RPM 0 it is silent.

public AudioParameter Rpm { get; }                       // value 0
public AudioParameter Load { get; }                      // value 1; null = on load
public int Cylinders { get; }                            // 1–16: the scale of order 1
public IReadOnlyList<AdditiveEngine.Order> Orders { get; } // order, offLoad, onLoad (0–1); the first 16 play
public float NoiseOffLoad { get; }                       // the noise's RMS off load
public float NoiseOnLoad { get; }                        // and on load
public float NoiseBrightness { get; }                    // 1–32 × the firing frequency
public const int MaxOrders = 16;

GeneratorCost

What Pepper’s generators cost on the mixing thread, measured where they run, a build included (for the Steam Deck). While Measuring is off, each generator buffer only checks a flag.

public static bool Measuring { get; set; }              // switching it on starts a new measurement
public static long Buffers { get; }                     // buffers measured, all generators together
public static double Milliseconds { get; }              // their time on the mixing thread
public static double MicrosecondsPerBuffer { get; }
public static double Seconds { get; }                   // since the measurement started
public static double CoreSharePerInstance(int instances) // 0–1
public static string Report(int instances)              // one line for the log
public static void Reset()

Generator containers also show in the tools: the Audition gives their parameters sliders and plays them outside Play mode, the Container Validator checks them (and counts them as loops for its Persistent note), and the debug windows name a generator voice “(generated)”.

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