Pepper.SFX
All Guidance pages

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 to Reverb_Bathroom at the container’s authored level, scaled by the listener’s audibility weight (1.0 same-zone, <1 through portals, 0 unreachable).
  • The Reverb_Bathroom bus 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 via AudioMixer.SetFloat; for a convolution bus it carries the native convolution plugin (see §15.14).
  • The bus’s character comes entirely from its ReverbSendBus asset. 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)
  • ReverbBus is the mixer group voices physically in this zone send to (when their container has Allow Reverb Send = ✓). Drag in the ReverbBus field from a ReverbSendBus asset. 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.ReverbProfile field was removed. A bus’s character lives entirely on its ReverbSendBus asset, 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, via entryFadeMeters, 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:

  1. 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).
  2. ExplicitReverbBus set: the explicit-bus short-circuit. The voice sends at its container’s ReverbSendLevelDb to 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.
  3. 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:

  1. Skip convolution buses — their character is the impulse response, not parametric params (the six Reverb_<Bus>_* params aren’t exposed on them).
  2. Read the bus’s own character from its ReverbSendBus asset via bus.GetDefaultProfile(scratch) into a non-allocating scratch profile. No zone lookup happens — the asset is the source of truth.
  3. 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’s ReverbProfile. The source-of-truth unification replaced all of that with the unconditional bus.GetDefaultProfile read above. FindFirstZoneFeedingBus is 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 old FindFirstZoneFeedingBus walk was removed in the source-of-truth unification). 5 buses × 20 Hz ≈ 600 SetFloats/sec. Negligible.
  • Static-emitter cache: voice.CachedSourceZone avoids the O(zoneCount) GetZoneContainingPoint walk every tick for voices marked StaticEmitter. 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

  1. 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.
  2. Six driven parameters, eight static. If you need Diffusion or Density to vary per-room, you’d extend AudioReverbProfile and add new Suffix* constants to ReverbSendBus. The architecture supports it; v1 just doesn’t ship it.
  3. One character per bus. A bus has a single acoustic identity (its ReverbSendBus asset). 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.
  4. 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:

  1. On the ReverbSendBus asset, tick Use Convolution. The parametric fields are ignored from here on.
  2. 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.
  3. Set Convolution Wet Trim (dB) to taste — this is the live, cheap level control for the wet path.
  4. 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.
  5. 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. AmbientEmitter uses a per-source parametric AudioReverbFilter, 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 Db isn’t at -∞ / -80 dB.
  • Confirm AudioManager.ReverbSendBusRegistry is assigned and non-empty.
  • Confirm the source’s zone has ReverbBus pointing at a ReverbSendBus whose ReverbBus (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 ReverbSendBus asset? 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 Room value — at -10000 mB the reverb is silent. Drag it toward 0 (the default).
  • Convolution bus: check it has an Impulse Response assigned 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 ReverbSendBus asset, not from any zone, so “wrong room” means the wrong asset is wired. Confirm the zone’s ReverbBus points 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 weapons bus 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.