Guidance / Manual
Bus & Mixing
6.1 Bus Architecture
Location: Events/AudioBus.cs
Purpose: Hierarchical volume control and mixing.
Properties
string BusName { get; } // Identifier
AudioBus ParentBus { get; } // Hierarchy
AudioMixerGroup MixerGroup { get; } // Unity AudioMixer integration
float VolumeDb { get; } // Volume in decibels (the asset's; SetVolume(dB) changes it)
float VolumeMultiplier { get; } // Linear multiplier (0-1)
float StateOffsetDb { get; } // What Offset states add now (§7), runtime only
float PlayerVolumeDb { get; set; } // The player's volume (§6.8), runtime only
int MaxRealVoices { get; set; } // Its voice cap (§6.9), 0 = none
int RealVoiceCount { get; } // Real voices of its sounds (and its children's) now
bool Mute { get; } // Mute this bus (SetMute)
bool IsMuted { get; } // Muted by itself or a parent
bool Solo { get; } // Solo this bus and its child buses (SetSolo; §6.3)
A bus’s level is Volume + state offset + player volume, in dB, times its multiplier, its ducking and its curves, times its parent’s. Only Volume is in the asset.
6.2 Decibel System
Why dB instead of 0-1?
Decibels match how humans perceive loudness (logarithmic, not linear).
Conversion Formulas:
// dB to linear
float linear = Mathf.Pow(10f, db / 20f);
// Linear to dB
float db = 20f * Mathf.Log10(linear);
Key dB Values:
+6 dB = 2x louder
0 dB = No change (1.0 linear)
-6 dB = ~0.5x quieter
-12 dB = ~0.25x quieter
-20 dB = ~0.1x quieter
-80 dB = Silence (effective zero)
6.3 Hierarchical Volume
Concept: Child buses inherit parent volume.
Master Bus: -3 dB (0.707 linear)
└── SFX Bus: 0 dB (1.0 linear)
└── Weapons Bus: +3 dB (1.41 linear)
Final Weapons Volume = 0.707 * 1.0 * 1.41 ≈ 1.0
Implementation:
public float GetFinalVolume()
{
float gain = ChainGain(out bool soloed); // this bus's level times its parents', one walk up the chain
return anySoloed && !soloed ? 0f : gain; // while any bus is soloed: heard only if it or a parent is
}
private float ChainGain(out bool soloed)
{
soloed = solo;
if (mute) return 0f; // mute wins, here or on any parent
float parent = 1f;
if (parentBus != null)
{
parent = parentBus.ChainGain(out bool parentSoloed);
soloed |= parentSoloed;
}
// The asset's Volume, the states' offset and the player's volume (§6.8), in dB: all three reach child buses.
float linearDb = Mathf.Pow(10f, (volumeDb + stateOffsetDb + playerVolumeDb) / 20f);
return volumeMultiplier * linearDb * duckingValue * curveGain * parent;
}
Mute and solo. Mute silences a bus and every bus under it. Solo works as on a mixing desk: while any bus is soloed, the soloed buses and their child buses are heard, through their parents, whose volume, mute and ducking still apply; every other bus is silenced. A parent of a soloed bus only passes the soloed bus through: sounds posted straight to that parent are silenced while the solo is on, as a desk mutes the other channels into a group. To hear a parent’s own sounds, solo the parent. Mute still wins: a muted soloed bus, or a muted parent of one, is silent. With no solo on, the check costs nothing.
Master
├── SFX
│ ├── Engines ← soloed: heard, through SFX and Master
│ │ └── Turbo heard (a child of a soloed bus)
│ └── Tyres silenced
└── Music silenced
Sounds posted straight to SFX or Master: silenced.
Solo is for authoring: the Mixer window’s and the Audio Profiler’s S buttons set it (Alt-click in the Mixer solos one bus and clears the rest), and Clear Solos turns them all off. A solo change reaches every sound already playing, paused and virtual ones too.
6.4 Ducking System
What: Lower the volume of target buses for a while when this bus’s ducking is triggered.
Use Case: Music ducks when dialogue plays.
Ducking is triggered, not driven by what the bus is playing: an event’s TriggerDucking action (its Fade Duration is the hold time) or a call to bus.TriggerDucking(holdSeconds) starts it (or targetBus.ApplyDucking(dB, attack, hold, release) from code). Each target then fades down over the attack, holds, and fades back up over the release. The duck runs on the target bus’s Ducking Time: Scaled (the default, game time, so a duck freezes part way while the game is paused) or Unscaled (real time, so it completes during a pause).
Ducks combine: the deepest wins. Each trigger is a duck of its own, and while several run on one bus it follows the deepest at each moment, so none cuts another short: nitro pressed twice gives one continuous duck released after the second press’s hold, and a short UI duck during a dialogue duck leaves the dialogue duck as it was. A new duck starts from the level the bus is at, so re-triggering the same duck sounds exactly as one envelope restarting did. (Before 3.1.0-dev.7 the last trigger replaced the duck in progress, so a short or shallow one could cut a long or deep one short.)
Configuration
bool EnableDucking { get; } // Enable ducking feature
List<DuckingTarget> DuckingTargets { get; } // Buses to duck
float DuckingAttack { get; } // Fade down time (default 0.05s)
float DuckingRelease { get; } // Fade up time (default 0.2s)
DuckingTarget Structure
[Serializable]
public class DuckingTarget
{
public AudioBus targetBus; // Bus to duck
public float duckAmount = -6f; // Attenuation in dB, -80..0 (-6 dB is about half the level)
}
Example Setup
Dialogue Bus:
- enableDucking: true
- duckingTargets:
- Music Bus, duckAmount: -10.5 dB (about 30% of the level)
- SFX Bus, duckAmount: -6 dB (about 50%)
- duckingAttack: 0.05s (quick duck)
- duckingRelease: 0.8s (slow return)
Dialogue event: Play the line, plus a TriggerDucking action with Fade Duration = the line's length (the hold)
Result: When the dialogue event fires, music quickly drops to about 30% of its level and SFX to about 50%. After the hold they return over 0.8s.
6.5 Unity AudioMixer Integration
Setup:
- Create AudioMixerGroup in Unity’s AudioMixer window
- Assign to bus’s
mixerGroupfield - System routes voices through mixer automatically
Benefits:
- Use Unity’s built-in effects (reverb, EQ, etc.)
- Visual mixing interface
- Snapshots and ducking (separate from Pepper.SFX ducking)
6.6 Parameter Curves on a Bus
A bus’s Parameter Curves move its mix with a parameter, without code: nitro’s punch sweeping the world’s low-pass, a split-screen mode taking 3 dB off the hero bus. They are the same curves as a container’s (§4.1), with three properties:
| Property | Unit | How it applies |
|---|---|---|
| Volume | dB | Multiplies into the bus’s level, like ducking: every sound on the bus and on its child buses follows. The bus’s own Volume setting is untouched. |
| Low Pass | Hz | A Lowpass effect on the bus’s Mixer Group: one filter for everything routed through that group (reverb tails and ambience included), however many sounds. |
| High Pass | Hz | The same with a Highpass effect. |
- Curves read the parameter’s global value (
AudioManager.SetParameter), after its smoothing, every frame. A bus is shared by every listening point, so an emitter’s or a sound’s own value doesn’t move it. A move tied to one car (the hero’s engine pulling back as it speeds up) is a curve on that car’s sound instead. - Filters need a Mixer Group and an exposed cutoff. In the bus inspector, Add Low Pass to Mixer Group (and Add High Pass) adds a Lowpass Simple (Highpass Simple) effect to the group, exposes its cutoff as
<Bus>_LowPassand fills in Low Pass Parameter. Or do it by hand: add the effect, right-click its Cutoff freq, Expose it, and type its exposed name. Without one, the curve logs a warning once and does nothing. - Pepper eases the cutoff over about 20 ms (in octaves) and writes it to the mixer only when it moves. Pitch curves are ignored on a bus.
Example: nitro's punch on the other cars
Parameter asset: NitroPunch (0-1, global)
Others bus: Mixer Group "Others"; Add Low Pass to Mixer Group
Parameter Curve: NitroPunch → Low Pass, 0 → 18000 Hz, 1 → 2000 Hz
Game code: AudioManager.Instance.RampParameter(nitroPunch, 1f, 0.3f), then back to 0
6.7 Pausing a Bus
For a game’s pause, pause the buses the world plays on, and leave the pause menu’s buses alone:
// The game pauses (time scale 0): the cars and the world hold, the pause menu's clicks and music play on.
worldBus.Pause(); // its child buses (Vehicles, Ambience...) pause with it
// ... and on resume:
worldBus.Resume();
- Pausing fades out. Every sound on the bus and its child buses fades out over about 25 ms and holds its place, keeping no real voice (so the pause menu’s sounds get them).
Pauseon its own would be a click. - Resuming carries on from where it was, fading back in, every layer of an engine in the same frame, so they stay in phase.
- Sounds posted to a paused bus follow its While Paused setting. Drop One-shots (the default): a sound that doesn’t loop is dropped before its first sample (a crash under the pause menu shouldn’t play later), and a loop starts paused and plays on resume. Hold: every sound starts paused and plays on resume.
- A child bus is paused while it or any parent is paused.
IsPausedsays so;Resumeon a child doesn’t override its parent’s pause. - It is a separate pause from
AudioHandle.PauseandAudioManager.PauseAll, which pause the sound at once, as before. A sound plays again only when none of them pauses it. - Timing during a pause: set the pause menu’s events to Time = Unscaled (§5.4) and the buses it ducks to Ducking Time = Unscaled (§6.4), so their cooldowns, delays and ducks run while the time scale is 0.
- A paused sound that isn’t Persistent can be stolen first if the pool runs short during the pause, since it is silent; a Persistent one never is.
6.8 Player Volume
A Settings page’s sliders (master, music, effects) set the player’s volume on each bus, not its Volume:
musicBus.PlayerVolumeDb = AudioExtensions.LinearToDb(musicSlider.value); // -80 … +24 dB, 0 = as mixed
AudioManager.Instance.MasterVolumeDb = -3f; // or SetMasterVolume(linear)
bool muted = AudioManager.Instance.IsAllMuted; // MuteAll's getter
- It is a level of its own, on top of the bus’s Volume and any state’s offset (§7), so a mix state never undoes it and it never undoes one.
- It is never saved to the asset, and each play session starts at 0 dB: the game saves the player’s settings itself and sets them again at boot. (A bus’s Volume, which
SetVolumeandSetBusVolumechange, is the asset’s: in the editor a change made in Play mode stays in it.) AudioManager.GetBus(name)finds a bus by name. An unknown name returns the master bus, as before, but now warns once;SetBusVolumewith an unknown name warns once and changes nothing (it used to set the master bus).Example08_AudioSettingsUIis a complete settings page.
6.9 A Bus’s Voice Cap
A bus can cap how many real voices its sounds use at once: Max Real Voices in its Voices section (0, the default, is no cap). A racing game caps the bus its other cars play on, so a pack of them can’t crowd out your own car:
othersBus.MaxRealVoices = 12;
int now = othersBus.RealVoiceCount; // real voices of its sounds and its child buses' now
- It counts the real voices of the sounds whose Target Bus is this bus or one of its children.
- Over it, the least important of those sounds go virtual, a whole sound at a time, exactly as over the Audio Manager’s Max Real Voices (§3.2, Voice Virtualization): when a sound starts, and in the voice pass, with the same stickiness. They keep their place and come back in step when there is room. A single sound with more voices than the cap keeps its most important ones.
- It never ends a sound, so it frees no voices in the pool: that is still the pool’s business (Real + Virtual + Steal Headroom).
- The Audio Manager’s Max Real Voices still applies on top. The bus inspector shows the live count in Play mode.
From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.