Guidance / Manual
Reverb Send Buses & Per-Zone Reverb
The “wet path” complement to the occlusion chapter. Where chapter 14 controlled how voices are muffled through geometry, this chapter controls how voices contribute to and hear the acoustic character of the rooms they’re in. Two subsystems, one shared infrastructure.
15.1 Why Per-Zone Reverb
A single global reverb sounds wrong everywhere. A bathroom and a cathedral need very different decay times, reflection densities, and room sizes; sticking one preset across the whole game means most rooms feel like the same generic space. Per-zone reverb fixes that — but naively, in two distinct ways that don’t compose.
Approach 1: per-source reverb. Each ambient bed and each SFX voice carries its own reverb settings. Right for ambient beds that should sound like the room they originate in (rain heard through a window should carry the outdoor character, not the room’s). Wrong for SFX that should sound like the room the source is in (a gunshot fired in the bathroom should keep its bathroom-reverb tail even when the listener walks into the hallway). And expensive: N reverb instances running simultaneously.
Approach 2: per-listener reverb. A single reverb whose settings update based on the room the listener is currently in. Cheap (one reverb), and right for some sounds — but completely wrong for “a gunshot fired in the bathroom while the player is in the hallway should still sound like a bathroom gunshot.” This approach makes every active sound sound like wherever the listener happens to be standing.
Neither is correct alone. Real acoustics combine both:
- Routing is source-driven: a voice’s contribution goes to the reverb of the room it’s physically in.
- Character is room-driven: each reverb bus represents one acoustic space, and its parameters don’t change just because the listener walked to a different room.
The Reverb Send Bus subsystem implements this split. Source-side: which bus a voice sends to is determined by the voice’s zone. Listener-side: each bus’s character is its own — defined by the bus’s ReverbSendBus asset, the single source of truth. Walking from the bathroom to the cathedral doesn’t change which bus the bathroom gunshot is sending to — and doesn’t change what the bathroom bus sounds like. Only the listener’s audibility weight to that bus changes (the muffled “I can still hear it through the wall” attenuation), and that’s handled by the propagation subsystem feeding the source-side audibility cache.
15.2 Mental Model
Two independent axes per voice:
ROUTING (source-side) — set on the AudioZone, decides which bus voices send to:
AudioZone "Bathroom" .ReverbBus = Reverb_Bathroom ──► send to Reverb_Bathroom bus
AudioZone "Cathedral" .ReverbBus = Reverb_Cathedral ──► send to Reverb_Cathedral bus
AudioZone "Hallway" .ReverbBus = (none) ──► no reverb sends from this zone
CHARACTER (listener-side) — set on the ReverbSendBus asset, the single source of truth:
ReverbSendBus "Reverb_Bathroom" ──► parametric params OR impulse response
ReverbSendBus "Reverb_Cathedral" ──► parametric params OR impulse response
(a bus's sound never depends on listener position or on any zone)
- Voices in the Bathroom zone whose container has
Allow Reverb Send = ✓send a copy of their dry signal toReverb_Bathroomat the container’s authored level, scaled by the listener’s audibility weight (1.0 same-zone, <1 through portals, 0 unreachable). - The
Reverb_Bathroombus is one mixer group on the project’s voice mixer. For a parametric bus it carries an SFX Reverb effect whose six listener-relevant parameters are exposed to the runtime viaAudioMixer.SetFloat; for a convolution bus it carries the native convolution plugin (see §15.14). - The bus’s character comes entirely from its
ReverbSendBusasset. For a parametric bus, the runtime driver writes the asset’s six parameters onto the mixer effect each tick. The character does not depend on the listener’s position or on any zone’s profile — the asset is the room’s identity.
15.3 The Two Halves Working Together
The clean separation between routing and character produces these expected behaviors:
| Scenario | Source-side (which bus) | Listener-side (bus character) | Audibility scaling |
|---|---|---|---|
| Listener in bathroom, gunshot in bathroom | Bathroom | Bathroom bus character | 1.0 (same zone) |
| Listener in hallway, gunshot in bathroom (door open) | Bathroom | Bathroom bus character | ~0.3 (portal loss) |
| Listener in hallway, gunshot in bathroom (door closed) | Bathroom | Bathroom bus character | ~0.05 (closed door) |
| Listener in cathedral, gunshot in bathroom (no path) | Bathroom | Bathroom bus character | 0 (unreachable) |
| Listener in cathedral, choir voice in cathedral | Cathedral | Cathedral bus character | 1.0 |
The bathroom gunshot keeps sounding like a bathroom gunshot regardless of where the listener stands — only its loudness changes based on the listener’s audibility to the bathroom. Two different voices in two different zones send to two different reverb buses, both running simultaneously, each carrying its own room’s character.
15.4 The ReverbSendBus Asset
One ScriptableObject per acoustic space identity in the project. Create via Project ▸ Create ▸ Audio System ▸ Reverb Send Bus, name it after the room (e.g. Reverb_Bathroom).
Three layers of fields:
Mixer wiring (top of inspector):
| Field | Purpose |
|---|---|
ReverbMixer |
The voice mixer this bus lives on. Required — the bus’s mixer group is auto-created under this asset. |
ReverbBus |
Auto-filled by Generate. The mixer group this SendBus owns. Leave null on first authoring. |
OutputDestination |
Optional. The mixer group this bus’s wet output routes to. Vestigial in single-mixer setups; left null is normal. |
ParentGroup |
Optional. Drops the auto-created group under this parent instead of directly under master. The “ReverbsWet” return-bus convention. |
Parametric reverb defaults (the SFX Reverb effect’s 14 parameters, split into Basic and Advanced):
- Basic (4):
DryLevel,Room,DecayTime,ReverbLevel— the parameters 80% of tuning happens on. - Advanced (10), behind an EditorPrefs-backed foldout that’s closed by default:
RoomHF,RoomLF,DecayHFRatio,ReflectionsLevel,ReflectionsDelay,ReverbDelay,HFReference,LFReference,Diffusion,Density.
These parametric defaults apply only when Use Convolution is off. When it’s on, the bus ignores them and uses the impulse response instead.
Convolution (the alternative to the parametric reverb — see §15.14 for the full walkthrough):
| Field | Purpose |
|---|---|
Use Convolution |
Off by default. When on, the bus uses a real impulse response (native convolution plugin) instead of the parametric SFX Reverb. The parametric fields above are ignored. |
Impulse Response |
The IR AudioClip to convolve with. Must be CPU-readable — set its import Load Type to Decompress On Load or PCM. Mono or stereo (downmixed to mono in v1). Required when Use Convolution is on, or the bus is silent. |
Convolution Wet Trim |
Wet output trim in dB (range -80…+20). 0 = unity. The live, cheap level control for the convolution wet path. |
A fourth convolution field, the IR slot index, is auto-assigned by Generate and hidden from the inspector — designers never set it.
The asset’s name is the bus id. Sanitization strips characters that would produce invalid Unity exposed-parameter identifiers; a bus named Cave (small) becomes Cavesmall for parameter naming. Rename the asset and the bus identity follows.
15.5 The ReverbSendBusRegistry Asset
One per project. A flat list of every ReverbSendBus asset in the project, used by:
- The runtime drivers, to enumerate which buses exist and which mixer parameters to write to.
- The Occlusion Layout Builder, to author one Send effect per registered bus on every occlusion slot.
Create via Project ▸ Create ▸ Audio System ▸ Reverb Send Bus Registry. Open the Reverb Send Buses window (Window ▸ Audio System ▸ Reverb Send Buses) and click Refresh — the registry auto-populates from every ReverbSendBus asset in the project.
Drop the registry into AudioManager ▸ Reverb Send Bus Registry. The runtime gates the reverb-sends driver on the registry being assigned and non-empty.
15.6 Authoring Workflow (ReverbAutoCreator + Reverb Send Buses Window)
Two complementary editor UIs:
Per-asset inspector — select a ReverbSendBus asset to see the custom inspector. Four buttons:
| Button | What it does |
|---|---|
| Generate Mixer Group | Creates or finds the bus’s mixer group and backfills ReverbBus on the SO. For a parametric bus: drops an SFX Reverb effect, writes the SO’s parametric defaults to the target snapshot, and exposes the six runtime-driven parameters under schema names. For a convolution bus (Use Convolution = on): drops the native SFX Convolution Reverb plugin instead, assigns a free IR slot, and authors its IR Slot / Wet Trim / IR Gain params. Switching the toggle and re-generating auto-cleans the previous effect. Idempotent. |
| Validate Wiring | Read-only health check. Reports “Wiring OK” or a bullet list of issues (missing mixer, missing effect, missing exposed param, mismatched OutputDestination, ParentGroup mismatch). |
| Apply Params → Mixer | Pushes the SO’s parametric values into the existing mixer effect without re-running the group/expose work. Use after tweaking parametric values on the SO that you want reflected on the mixer side. |
| Pull Params ← Mixer | Copies the mixer effect’s current values back onto the SO. Use after tweaking the mixer-window sliders that you want reflected on the SO side. |
Project-wide window — Window ▸ Audio System ▸ Reverb Send Buses:
| Button | What it does |
|---|---|
| Create New Bus | Save File panel; creates a fresh ReverbSendBus asset at the chosen path. |
| Refresh | Re-scans the project for SendBus assets and re-runs Validate on each. |
| Validate All | Same as Refresh but with a summary HelpBox. |
| Generate All Missing | For every bus whose ReverbBus is null but whose ReverbMixer is assigned, runs EnsureSendBus. Buses with ReverbBus already filled are left alone (per-asset re-generation lives on the inspector). |
Each row in the list shows the bus’s name, its assigned Mixer, its assigned Bus group (or (not generated)), and any Validate issues (collapsible).
15.7 Six Exposed Parameters, Not Fourteen
Unity’s SFX Reverb effect has 14 parameters. Generate exposes six:
| Suffix | Parameter | What it drives |
|---|---|---|
_Wet |
Reverb (late reverb dB) |
The diffuse tail level |
_Decay |
Decay Time (seconds) |
Reverb tail length |
_Room |
Room (dB) |
Overall wet-signal gain |
_RoomHF |
Room HF (dB) |
High-frequency tilt on the wet path |
_DecayHFRatio |
Decay HF Ratio (ratio) |
How fast highs decay relative to mids |
_Reflections |
Reflections (dB) |
Early reflections gain |
These match the 1:1 field set on AudioReverbProfile — they’re the six parameters that describe a room’s identity in a way that varies meaningfully between rooms. Cathedral and bathroom differ on these six.
The other eight (DryLevel, RoomLF, ReflectionsDelay, ReverbDelay, HFReference, LFReference, Diffusion, Density) are static shaping rather than runtime-driven character — they describe how this reverb is shaped and stay at the SO-authored snapshot values, not written each tick.
The exposed-parameter names follow the schema Reverb_<BusName>_<Suffix>. The constants live on ReverbSendBus (SuffixWet, SuffixDecay, …) so authoring and runtime can’t drift out of name agreement.
This whole six-parameter mechanism applies to parametric buses only. A convolution bus exposes no SFX Reverb params (its character is the impulse response) and is skipped by the listener-side driver — see §15.14.
15.8 The AudioZone Side
The zone’s connection to the reverb subsystem is routing only — a single field:
public AudioMixerGroup ReverbBus; // routing target (source-side)
ReverbBusis the mixer group voices physically in this zone send to (when their container hasAllow Reverb Send = ✓). Drag in theReverbBusfield from aReverbSendBusasset. Leave null for zones that don’t have a dedicated reverb (the source-side send is then skipped for sources in that zone).
Source-of-truth note (changed 2026-05-21). The zone no longer carries any reverb character — the old
AudioZone.ReverbProfilefield was removed. A bus’s character lives entirely on itsReverbSendBusasset, which is now the single source of truth. To make a room “sound reverberant,” tune the parameters (or assign an IR) on the bus asset, not on the zone. The zone only decides which bus voices route to (and, viaentryFadeMeters, how that send fades across the zone boundary).
Multiple zones can feed the same bus (e.g. Cathedral and Cathedral_Crypt both pointing at Reverb_Cathedral) — useful for routing several rooms to one shared acoustic identity. Because character is now per-bus, every zone feeding a given bus produces that bus’s single character; there is no per-zone variation on a shared bus. If you need two rooms to sound different, give them separate bus assets.
15.9 The Source-Side Runtime: AudioManager.ReverbSends
The “which bus, how loud” half. Runs at reverbSendsUpdateInterval (default 50 ms = 20 Hz) from a coroutine alongside the listener-side driver. Per tick, per voice with a slot:
- No container or
AllowReverbSend = false: every authored send on this slot gets silenced (so stale levels from a prior tick or prior voice don’t leak). ExplicitReverbBusset: the explicit-bus short-circuit. The voice sends at its container’sReverbSendLevelDbto the explicit bus only; all other registered buses get silenced. Use for sounds that need fixed acoustic character regardless of where the listener stands — music, narrator VO, scripted UI tails, cathedral choir always sounding like a cathedral.- Otherwise — zone-driven routing:
- Resolve the source’s current zone (cached for static emitters, re-resolved each tick for movers).
- Look up listener audibility to that zone from the propagation cache: same-zone = 1.0, through portals = the linear-amplitude chain product, unreachable = 0.
- Fold in source membership (
AudioZone.GetMembershipFactor) so voices near a zone’s surface taper smoothly across the boundary instead of flipping. - For each registered bus, compute
db = baseDb + 20·log10(audibility × membership)if and only if the source’s zone feeds this bus; else write the silent floor (-80 dB).
The per-tick AudioMixer.SetFloat on the slot’s SendLevelParams[bus.BusName] writes the result.
Immediate-on-Play write: AudioContainer.ConfigureAudioSource calls WriteReverbSendsImmediate synchronously, so a voice’s first audio frame already carries correct reverb routing. Without this, transient sounds (gunshots, single footstep clicks) on static emitters would play dry for up to one driver tick.
15.10 The Listener-Side Runtime: AudioManager.ReverbBusParams
The “what does each bus sound like” half. Same coroutine, same cadence. Per tick, per registered bus:
- Skip convolution buses — their character is the impulse response, not parametric params (the six
Reverb_<Bus>_*params aren’t exposed on them). - Read the bus’s own character from its
ReverbSendBusasset viabus.GetDefaultProfile(scratch)into a non-allocating scratch profile. No zone lookup happens — the asset is the source of truth. - Write six
AudioMixer.SetFloats to the bus’s exposed parameters (Reverb_<Bus>_Wet,_Decay,_Room,_RoomHF,_DecayHFRatio,_Reflections).
Each bus has one stable acoustic identity that never depends on listener position — only the source-side audibility (loudness) changes when the listener moves. Re-driving every tick (rather than once) is belt-and-suspenders against exposed-param fallback ambiguity; it’s six SetFloats per parametric bus, negligible for a handful of buses.
Changed 2026-05-21. This driver used to resolve a “canonical zone” per bus (listener’s zone, else first-registered via
FindFirstZoneFeedingBus, else SO defaults) and write that zone’sReverbProfile. The source-of-truth unification replaced all of that with the unconditionalbus.GetDefaultProfileread above.FindFirstZoneFeedingBusis no longer used by this path.
15.11 Container Opt-Ins, Revisited
The container flags that interact with this subsystem (defined in chapter 14, repeated here for context):
| Container flag | Effect on the reverb-send path |
|---|---|
Allow Reverb Send |
Master opt-in. When off, voices from this container never send to any reverb bus. |
Reverb Send Level Db |
Per-container baseline level (dB) for the send. 0 dB = “as loud as dry”, -∞ dB ≈ silent. |
Static Emitter |
Optimization: skip the source-zone resolve every tick for non-moving voices. |
Explicit Reverb Bus |
Override the zone-driven routing entirely. Voice always sends to this bus at the authored dB. |
The Reverb Send Level Db slider on the container is the right place to dial individual sounds in or out of the wet mix without touching the zone’s character — gunshots usually want a healthy send (0 to -3 dB), footsteps usually want quieter (-12 to -20 dB), UI clicks want either no reverb at all (Allow Reverb Send = off) or a tight UI-only reverb via Explicit Reverb Bus.
15.12 Performance
- Reverb-sends driver cost: O(voiceCount × busCount) per tick. Default 20 Hz × 32 real voices × (say) 5 buses = 3200
SetFloats/sec. Negligible. - Bus-params driver cost: O(busCount) per tick — six
SetFloats per parametric bus, convolution buses skipped, no zone scan (the oldFindFirstZoneFeedingBuswalk was removed in the source-of-truth unification). 5 buses × 20 Hz ≈ 600SetFloats/sec. Negligible. - Static-emitter cache:
voice.CachedSourceZoneavoids the O(zoneCount)GetZoneContainingPointwalk every tick for voices markedStaticEmitter. Footsteps parented to the player don’t get this — moving emitters re-resolve every tick. reverbSendsUpdateInterval(default 0.05 s = 20 Hz) trades responsiveness for CPU. Drop to 0.02 s (50 Hz) only if you have fast projectiles crossing zone boundaries and have measured the CPU is fine.
15.13 Design Limits
- One reverb effect per bus. The schema assumes a single SFX Reverb on each generated mixer group. Stacking multiple reverbs on a bus is possible but not surfaced through the SO; you’d lose the Apply/Pull round-trip with the SO and the auto-exposed parameter set.
- Six driven parameters, eight static. If you need
DiffusionorDensityto vary per-room, you’d extendAudioReverbProfileand add newSuffix*constants toReverbSendBus. The architecture supports it; v1 just doesn’t ship it. - One character per bus. A bus has a single acoustic identity (its
ReverbSendBusasset). Several zones can route to one bus, but they all get that one character — there is no per-zone variation on a shared bus, and no mixing-by-distance between zones. If two rooms must sound different, author two bus assets. - Convolution is Windows-only and in beta. The native convolution path (§15.14) ships for Windows x64 only — macOS and consoles are Phase 4. It’s feature-complete and build-validated for that scope, but carries known caveats: no runtime IR swap (the IR is fixed at author/scene-load time), ambient emitters can’t render convolution (they fall back to inert parametric defaults — §15.14), and the convolver rebuilds on the audio thread (fine at load time, not yet ready for in-game IR changes).
15.14 Convolution Reverb (Windows, beta)
Convolution is the alternative engine for a reverb bus. Instead of Unity’s parametric SFX Reverb (which approximates a room from a handful of knobs), a convolution bus convolves the dry send with a real impulse response (IR) — a recording of how an actual space (or an external hardware/plugin reverb) responds to an impulse. This captures the true character of cathedrals, stairwells, parking garages, or a favorite Lexicon patch in a way the parametric reverb can’t.
It’s an opt-in, per-bus choice: parametric stays the cheap default for ordinary zones; convolution is the path for hero spaces.
Status: beta, Windows x64 only. Feature-complete and validated in a standalone Windows build, but see the caveats at the end of this section before relying on it. On every other platform a convolution bus is silent; see Build Compatibility.
When to use which:
| Use parametric (SFX Reverb) | Use convolution |
|---|---|
| Generic / cheap zones, lots of them | Hero spaces where the room’s identity matters |
| You want to tune by ear with knobs | You have (or can capture) a real IR of the space |
| Cross-platform today | Windows only for now |
| Live parameter changes are fine | IR is fixed at author/scene-load time (no in-game swap) |
Authoring a convolution bus:
- On the
ReverbSendBusasset, tick Use Convolution. The parametric fields are ignored from here on. - Assign an Impulse Response clip. Set its import Load Type to Decompress On Load or PCM so the CPU can read its samples — without this the runtime upload fails (
GetData failed). Mono or stereo (downmixed to mono in v1); ~4 s at 48 kHz is a typical ceiling for SFX. - Set Convolution Wet Trim (dB) to taste — this is the live, cheap level control for the wet path.
- Click Generate Mixer Group on the asset. This drops the native SFX Convolution Reverb plugin on the bus group (instead of SFX Reverb), assigns the bus a free IR slot, and authors the plugin’s IR Slot / Wet Trim / IR Gain params.
- Wire the send path the same way as a parametric bus: run the Occlusion Layout Builder’s Auto-Create so each slot gets a Send and the bus group gets a Receive. Without this the bus is silent with no error — see §15.6 / the workflow note. (This step is identical for parametric and convolution buses; convolution didn’t change the send/receive plumbing.)
Runtime behavior: the IR samples are uploaded into the plugin’s slot table at AudioManager startup (UploadConvolutionIRs() in Awake). The slot table is volatile, so the IRs re-upload cleanly on every play / scene reload. The authored slot index + Wet Trim / IR Gain params live in the mixer asset; the IR samples come from this runtime upload. Nothing depends on driving exposed params from script at runtime.
Requirements: the native plugin SFXConvolutionReverb.dll ships under RunTime/Plugins/Windows/x86_64/ with Load on startup ticked in its import settings. Leave it on: without it the effect never registers in the mixer, and nothing warns you. The plugin runs on 64-bit Windows only; see Build Compatibility for what happens on other platforms.
Caveats (beta):
- No runtime IR swap. The IR is fixed once uploaded. Changing IRs during gameplay (zone-driven IR selection, crossfade) is Phase 4 and requires moving the convolver rebuild off the audio thread first.
- Ambients can’t render convolution.
AmbientEmitteruses a per-source parametricAudioReverbFilter, not the convolution plugin. An ambient whose listener-zone routes to a convolution bus gets that asset’s (inert) parametric defaults — not the impulse response. Treat ambients as parametric-only for now, or route them through the bus’s send/receive. - Convolver rebuild is on the audio thread. Fine because IRs only (re)assign at author / scene-load time, where the one-time hitch is inaudible. It would glitch if IRs swapped mid-gameplay — hence the no-runtime-swap caveat above.
- Malformed/empty IR clips aren’t fully guarded yet. Trust your IR clips; a bad clip’s behavior is undefined pending an input-validation pass.
15.15 Troubleshooting
“Voices play but no reverb tail.”
- Confirm
Allow Reverb Send = ✓on the container. - Confirm the container’s
Reverb Send Level Dbisn’t at -∞ / -80 dB. - Confirm
AudioManager.ReverbSendBusRegistryis assigned and non-empty. - Confirm the source’s zone has
ReverbBuspointing at aReverbSendBuswhoseReverbBus(the mixer group) is in the registry. - Confirm the listener can reach the source’s zone (a zone unreachable via portals produces audibility = 0).
“A bus has no character — no decay, no reflections.”
- Did you run Generate on its
ReverbSendBusasset? Until you do, the mixer group has no effect on it. - Click Validate Wiring on the asset. It’ll list every missing piece.
- Parametric bus: check the asset’s
Roomvalue — at-10000mB the reverb is silent. Drag it toward 0 (the default). - Convolution bus: check it has an
Impulse Responseassigned and that the clip is CPU-readable (Load Type = Decompress On Load / PCM). See §15.14.
“The bus sounds like the wrong room.”
- Character comes from the bus’s own
ReverbSendBusasset, not from any zone, so “wrong room” means the wrong asset is wired. Confirm the zone’sReverbBuspoints at the bus group you think it does, and that you tuned (or assigned the IR to) that same asset. - Remember several zones routing to one bus all share that bus’s single character — that’s by design, not a bug.
“After re-generating, my mixer-window manual tweaks were wiped.”
- Generate runs
WriteParametricDefaults, which pushes the SO’s values into the mixer effect’s target snapshot. If you’ve been tuning in the mixer window directly, run Pull Params ← Mixer before the next Generate to bring the SO into sync. After that, Generate is non-destructive — it writes back what’s already there.
“Voices in zone A still send to bus A’s reverb after I changed zone A’s ReverbBus.”
- The reverb-send driver re-resolves source zones every tick (or once-on-acquire for static emitters). Static-emitter voices in flight at the moment of the change still hold their cached zone. Stop and re-play them, or set
Static Emitter = ✓only on voices that genuinely don’t change zones.
“Pool exhaustion warning on a bus that previously worked.”
- Slot pool exhaustion is the same warning shape from chapter 14 — see that troubleshooting block. Reverb-send voices and occlusion voices share the same per-bus slot pool, so a
weaponsbus configured for 6 slots can handle 6 combined occluded-or-reverb-sending voices, not 6 of each.
From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.