-
Notifications
You must be signed in to change notification settings - Fork 8
WorldAudio Subsystem
Importance-based audio emitter management for AudioSource components in the scene.
The WorldAudio subsystem manages AudioSources that play in the world (music, ambient sound, sound effects) — as opposed to player voice audio, which is handled by PlayerAudioController.
It provides:
- Importance-based prioritization — closer/louder emitters update more frequently
- Partition-based time-slicing — distributes CPU budget across frames
- Automatic culling — beyond a configurable limit, low-importance emitters are suspended
- Privacy channel gating — emitters respect the same privacy channels as player voice
- Reverb and lowpass — applies distance-based audio effects per emitter
In VRChat, the measured limit is 32 active AudioSources with a single player in the world (probably ~64 virtual voices). When this limit is exceeded, Unity stops playing new AudioSources — they enter a "neither playing nor virtual" state where they simply don't produce sound, even when the player walks right up to them. There's no automatic recovery; once an AudioSource is stuck in this state, it stays silent.
The WorldAudio subsystem solves this by:
- Active culling — instead of letting Unity randomly fail AudioSources, the system explicitly suspends low-importance emitters (distant/quiet) to keep the active count within budget. When a culled emitter becomes important (player approaches), it is reactivated and resumes playback seamlessly
- Dynamic reverb and occlusion — applies per-emitter reverb and lowpass filters based on distance and environment, which Unity does not provide out of the box
- Privacy channels — restricts which AudioSources a player can hear based on room/channel assignment, something Unity's audio system has no concept of
Without this system, world creators would need to manually manage AudioSource activation, implement their own distance-based culling, and build custom reverb/occlusion logic.
UdonVoiceUtils has two independent audio systems running side by side:
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ PlayerAudioController │ │ WorldAudioController │
│ (Player Voice) │ │ (AudioSource Emitters) │
│ │ │ │
│ • VRCPlayerApi-based │ │ • AudioSource-based │
│ • Processes remote players │ │ • Processes scene emitters │
│ • Voice range, gain, etc. │ │ • Importance sorting │
│ • Runs independently │ │ • Reads config from PAC │
│ • Occlusion, directionality │ │ • Partition-based updates │
│ • Privacy channels │ │ • Privacy channel gating │
└─────────────────────────────────┘ └─────────────────────────────────┘
▲ │
│ │
└──── reads config + override ───────┘
-
PlayerAudioController handles player voice audio via
VRCPlayerApi. It runs independently — you can use it without WorldAudio. -
WorldAudioController manages AudioSource emitters in the scene. It reads configuration from
PlayerAudioController(occlusion settings, local override) but PlayerAudioController does not depend on it.
Each OccludedEmitter component in the scene registers itself with the EmitterRegistry on startup. When an emitter is destroyed, it unregisters automatically.
Each frame, the system recalculates importance for a subset of emitters. Importance is based on:
- Distance from the local player's head
- Volume of the AudioSource
- Priority setting of the AudioSource
- Rolloff mode (logarithmic, linear, or custom)
Closer, louder emitters get lower importance scores (sorted ascending in the AVL tree). The most important emitters (lowest scores) are at the front of the tree.
Emitters are divided into partitions (buckets) with exponential sizes:
Partition 0: [1 emitter] ← highest priority, updates every frame
Partition 1: [4 emitters] ← updates frequently
Partition 2: [16 emitters] ← updates less often
Each frame, the cursor cycles through partitions and returns the next emitter needing an update. This distributes the update budget so not all emitters are processed simultaneously.
Configure via PartitionState:
-
Partitions(1–10) — number of buckets -
PartitionGrowthFactor(2–6) — size multiplier between partitions
For each emitter selected by the partition cursor:
- Privacy check — is this emitter audible to the local player?
- Occlusion — raycast from listener to emitter, calculate clarity
-
Culling — if emitter index ≥
MaxActiveSources, suspend it - Apply effects — update reverb, lowpass filter, spatial blend
When DisableGameObjectWhenCulled is enabled (default), culled emitters have their GameObject deactivated. This saves CPU on AudioSources that Unity videoplayers control. Reactivated when the emitter becomes active again.
| Field | Default | Description |
|---|---|---|
ImportanceUpdatesPerFrame |
1 |
How many emitters to recalculate importance for each frame. Higher = more responsive, more CPU |
UpdatesPerFrame |
3 |
How many emitters to update (apply effects) each frame. Higher = more responsive, more CPU |
MaxActiveSources |
32 |
Maximum concurrent active emitters. Beyond this, lowest-importance emitters are culled |
| Field | Default | Description |
|---|---|---|
DisableGameObjectWhenCulled |
true |
Deactivate GameObject when culled. Set to false for Unity videoplayer sources — disabling breaks audio filters |
PrivacyChannelId |
-1 |
Privacy channel for this emitter. -1 = no privacy (always audible) |
AudioSources managed by the WorldAudio system cannot be directly controlled by world creators. All play/pause/stop control must go through AudioSourceStateController. Do not call AudioSource.Play()/Stop() directly.
Direct manipulation of managed AudioSources (Play/Pause/Stop/clip changes) conflicts with:
- Culling and reactivation (position tracking breaks)
- Importance sorting (volume/priority changes affect scoring)
- Partition-based updates (state becomes inconsistent)
If you need to control an AudioSource directly, do not attach OccludedEmitter to it.
Only works with Unity-based video players. The WorldAudio system does not work with AVPro video players. AVPro manages its own audio pipeline and does not use Unity's AudioSource component in a way that is compatible with the emitter lifecycle management. Use Unity's built-in video player if you need WorldAudio to manage the audio.
| Component | Purpose |
|---|---|
| WorldAudioController | Core controller — drives the update loop |
| OccludedEmitter | Per-emitter lifecycle management |
| EmitterRegistry | Tracks active emitters |
| PartitionState | Partition configuration |
| PartitionCursor | Round-robin iteration |
| ReverbController | Reverb/lowpass per emitter |
| AudioSourceStateController | Play/pause/cull state machine |
- PlayerAudioController — the player voice system (runs independently)
- Audio Processing Pipeline — how player voice is processed
- Occlusion — how raycast-based occlusion works for both systems
- Architecture
- Occlusion
- Voice Directionality
- Height Scaling
- Audio Processing Pipeline
- Private Voice Channels
- WorldAudio Subsystem
- WorldAudio Limitations
- Set Up Occlusion
- Set Up Voice Channels
- Set Up Reverb
- Set Up Height Scaling
- Set Up Voice Directionality
- Set Up Microphone
- Set Up WorldAudio Emitters
- Enable Debug Mode
- Tutorial #1: Custom Voice Override Zone
- Tutorial #2: Private Voice Channels
- Tutorial #3: Setting Up World Audio
- PlayerAudioController
- PlayerAudioOverride
- PlayerAudioOverrideList
- PlayerAudioView
- PlayerAudioConfigurationModel
- SyncedPlayerAudioConfigurationModel
- VoiceUtils
- IgnoredPlayers
- AudioObstacle
- DefaultPlayerOcclusion
- NullPlayerOcclusion
- PlayerOcclusionStrategy
- Privacy Channel Reference
- AdjustableGain
- DynamicPrivacy
- PickupMicrophone — MVC microphone system with MicModel/MicController/MicView
- VoiceOverrideTriggerZone
- VoiceOverrideRoom