Unity Audio System
Prime Audio
System
Made by Azen
Complete audio architecture for Unity. Covers pooling, entity-bound channels, music crossfade, surface footsteps, RTPC, VO queues and more — all through one clean service interface.
Overview
Features
🎯
Pool-Based Playback
Per-category 2D/3D AudioSource pools with priority eviction. No garbage, no audio source sprawl.
Core
🔗
Entity-Bound Audio
AudioController base class — channels follow the entity transform in 3D, auto-cleanup on Destroy.
Core
🎛️
AudioHandle
Every Play() returns a handle. Stop, Pause, Resume, SetVolume any specific sound after firing it.
Core
🎲
Clip Variation
AudioClipGroup with Random, RandomNoRepeat, Sequential. Per-instance counters on shared SOs.
Core
🌀
Seamless Loop
DSP-time dual-source crossfade. No Unity loop gap. Intro → Loop → Outro pattern for engines/music.
Core
🎼
Adaptive Music
Synchronized stems fade in/out by game state. DOOM-style combat layers, NieR area discovery.
Music
🎺
Stinger System
Short transitional cues — Overlay, DuckAndPlay, CutAndReturn. Kill confirmed, level complete stabs.
Music
📐
RTPC
AudioParameter SO drives Volume / Pitch / LowPass / SpatialBlend via AnimationCurve. Engine RPM → pitch.
Advanced
👟
Surface Audio
PhysicsMaterial / Tag / TerrainLayer → footstep groups. Terrain reads dominant texture layer.
Advanced
🔕
Voice Limit + Cooldown
Max N simultaneous per category. Retrigger cooldown stops machine-gun SFX from Update().
Advanced
📡
Audio LOD
Distance culling before pool allocation. Auto-priority by distance. 50 off-screen NPCs cost nothing.
Advanced
🌍
Audio Zones
No-code ambient trigger. Place on Collider, assign clip, done. Distance fade via AnimationCurve.
Advanced
🗣️
VO Queue
Queue / Interrupt / Discard policies. Sequential VO playback. OnLineStart/End hooks for subtitles.
Advanced
📦
Auto Ducking
Rule-based: Dialogue → duck Music by -12dB in 0.1s, recover in 0.5s. Inspector-only config.
Advanced
🔌
Pluggable Deps
Addressables, any event bus, custom surfaces — all optional. Zero-config out of the box.
Integration
📱
App Focus Handler
Auto-mute on phone call / alt-tab. AudioListener pause on app background. No silent battery drain.
System
🛠️
Batch Clip Generator
Batch-generate AudioClipData from raw clips. Smart category and 3D detection from folder names.
Editor
🔒
Null Safe
Removing AudioService never throws. Everything falls back to NullAudioService — silent no-ops.
System
Setup
Installation
Requires: Unity 2022.3+ · DOTween (required, all fades) · Addressables (optional)
1
Install DOTween
Asset Store → DOTween (HOTween v2) → Import. Free version is enough.
Window → Package Manager → My Assets → DOTween → Import
2
Copy scripts
Drop the Scripts/ folder anywhere under Assets/. The Editor subfolder must stay inside a folder named Editor.
Assets/PrimeAudio/
Scripts/ ← core scripts here
Scripts/Editor/ ← must be named Editor
3
Enable Addressables (optional)
Only if you want async clip loading. Skip if using direct AudioClip references.
Window → Package Manager → Add package → com.unity.addressables
Project Settings → Player → Scripting Define Symbols → add: PRIME_AUDIO_ADDRESSABLES
Setup
Scene Setup
Create one AudioManager GameObject in your bootstrap/persistent scene. Attach these components:
AudioManager GameObject — required components
AudioService ← assign AudioMixer + AudioBank in Inspector
MusicService ← assign MixerGroup for music
AudioListenerGuard ← prevents duplicate AudioListener errors
AudioApplicationHandler ← auto-mute on phone call / alt-tab (mobile)
AudioManager GameObject — optional components
DuckingSystem ← auto volume ducking (Dialogue → duck Music)
AudioDebugOverlay ← press F10 in play mode to see pool stats
AdaptiveMusicSystem ← only if using vertical stem layering
AudioMixer Setup
Create one AudioMixer. Add a MixerGroup per audio category. Expose volume parameters with the naming convention used in your CategoryGroup config.
Typical AudioMixer Groups
Master
├── SFX
├── Music
├── Voice
└── Ambient
AudioCategoryID Setup
Create a AudioCategoryID ScriptableObject per category via Assets → Create → Audio → CategoryID. Then assign them in AudioService → Category Groups.
Bootstrap Code
GameBootstrap.cs — call once before any audio
private void Awake()
{
var audioService = GetComponent<AudioService>();
PrimeAudioBootstrap.Initialize(audioService);
}
Unity vs Prime Audio
Playing Sounds
The most basic operation — fire a sound and forget. Standard Unity requires you to manage AudioSources manually. Prime Audio routes everything through a pool.
One-shot sound
// Need AudioSource on GameObject
[SerializeField] private AudioSource _source;
[SerializeField] private AudioClip _clip;
_source.PlayOneShot(_clip);
// No AudioSource needed on this object
[SerializeField] private AudioClipData _clip;
_audio.Play(_clip);
Play with fade in
// Manual coroutine
IEnumerator FadeIn(AudioSource src, float dur)
{
src.volume = 0f;
src.Play();
while (src.volume < 1f) {
src.volume += Time.deltaTime / dur;
yield return null;
}
}
// Built-in, one line
_audio.Play(_clip, fadeInDuration: 1f);
// Or with builder:
_audio.With(_clip).FadeIn(1f).Play();
UI button click (no AudioSource on button needed)
// Every button needs AudioSource reference
public AudioSource uiAudioSource;
public AudioClip clickClip;
public void OnClick()
{
uiAudioSource.PlayOneShot(clickClip);
}
// Static call, no references needed
public void OnClick()
{
GlobalAudio.UI("btn_click");
// or:
GlobalAudio.UI(_clickClip);
}
Unity vs Prime Audio
Looping & Playback Control
Looping ambient sound
[SerializeField] private AudioSource _ambient;
void Start()
{
_ambient.loop = true;
_ambient.Play();
}
void StopAmbient()
{
_ambient.Stop();
}
private AudioHandle _ambientHandle;
void Start()
{
_ambientHandle = GlobalAudio.PlayLoop(_ambientClip);
}
void StopAmbient()
{
_ambientHandle.Stop(fadeOut: 1f);
}
Pause / Resume a specific sound
// Must keep AudioSource reference
[SerializeField] AudioSource _engineSource;
void PauseEngine() => _engineSource.Pause();
void ResumeEngine() => _engineSource.UnPause();
// Handle returned from Play()
private AudioHandle _engineHandle;
void StartEngine()
=> _engineHandle = _audio.Play(_engineClip, loop: true);
void PauseEngine() => _engineHandle.Pause();
void ResumeEngine() => _engineHandle.Resume();
Fade out and stop
// Manual coroutine every time
IEnumerator FadeOut(AudioSource src, float t)
{
float start = src.volume;
for (float f = 0; f < t; f += Time.deltaTime)
{
src.volume = Mathf.Lerp(start, 0, f/t);
yield return null;
}
src.Stop();
}
// DOTween fade built in
_engineHandle.Stop(fadeOut: 2f);
// or change volume mid-play:
_engineHandle.SetVolume(0.3f, fadeDuration: 0.5f);
Unity vs Prime Audio
3D Positional Audio
One-shot 3D sound at world position (explosion, impact)
// AudioSource.PlayClipAtPoint creates
// a new GameObject every call → GC
AudioSource.PlayClipAtPoint(
clip, hitPoint, volume: 0.8f);
// Reuses pooled AudioSource, zero GC
_audio.Play3D(hitPoint, _impactClip);
// or via builder:
_audio.With(_impactClip).At(hitPoint).Play();
Sound that follows a moving object
// AudioSource on same GameObject
// OR update position every frame
void Update()
{
_engineSource.transform.position
= transform.position;
}
// AudioController + IAudioEntity3DEntity
// Channel is parented to entity Transform
// Follows automatically, no Update needed
public Transform Transform => transform;
void StartEngine() => Play(_engineClip, loop: true);
Unity vs Prime Audio
Entity Audio (Characters, Enemies, Props)
The biggest architectural difference. Standard Unity puts AudioSource references directly in game logic. Prime Audio separates audio completely into a controller.
Enemy that plays alert, hit and death sounds
// Enemy.cs mixes game logic + audio
public class Enemy : MonoBehaviour
{
[SerializeField] AudioSource _src;
[SerializeField] AudioClip _alertClip;
[SerializeField] AudioClip _hitClip;
[SerializeField] AudioClip _deathClip;
void OnSpotPlayer()
=> _src.PlayOneShot(_alertClip);
void OnHit()
=> _src.PlayOneShot(_hitClip);
void OnDie()
{
_src.Stop();
_src.PlayOneShot(_deathClip);
}
}
// Enemy.cs — zero audio knowledge
public class Enemy : MonoBehaviour
{
[SerializeField]
private EnemyAudioController _audio;
void OnSpotPlayer() => _audio.PlayAlert();
void OnHit() => _audio.PlayHit();
void OnDie() => _audio.PlayDeath();
}
// EnemyAudioController.cs — only audio
public class EnemyAudioController
: AudioController, IAudioEntity3DEntity
{
[SerializeField] AudioClipGroup alertGroup;
[SerializeField] AudioClipData hitClip;
[SerializeField] AudioClipData deathClip;
public Transform Transform => transform;
private static class Slot {
public static readonly object
Alert = nameof(Alert),
Hit = nameof(Hit),
Death = nameof(Death);
}
public void PlayAlert() => Play(alertGroup, Slot.Alert);
public void PlayHit() => Play(hitClip, Slot.Hit);
public void PlayDeath()
{
StopAll(fadeOut: 0.1f);
Play(deathClip, Slot.Death);
}
}
Why this matters: Enemy.cs doesn't import any audio namespace. Swap the entire audio system — Enemy.cs doesn't change. Add new sounds — Enemy.cs doesn't change. Auto cleanup on Destroy — no leaking AudioSources.
Unity vs Prime Audio
Music & Crossfade
Switch music track with crossfade
// Manual coroutine, 2 AudioSources
[SerializeField] AudioSource _srcA, _srcB;
bool _usingA = true;
IEnumerator Crossfade(AudioClip next, float t)
{
var fadeOut = _usingA ? _srcA : _srcB;
var fadeIn = _usingA ? _srcB : _srcA;
fadeIn.clip = next;
fadeIn.volume = 0f;
fadeIn.Play();
for (float f=0; f<t; f+=Time.deltaTime) {
fadeOut.volume = Mathf.Lerp(1,0,f/t);
fadeIn.volume = Mathf.Lerp(0,1,f/t);
yield return null;
}
fadeOut.Stop();
_usingA = !_usingA;
}
// One line
_music.CrossfadeTo(combatMusic, duration: 2f);
Pause menu — switch to pause music and come back
// Must remember what was playing
private AudioClip _previousClip;
private float _previousTime;
void OpenPauseMenu()
{
_previousClip = _music.clip;
_previousTime = _music.time;
_music.clip = _pauseMenuClip;
_music.Play();
}
void ClosePauseMenu()
{
_music.clip = _previousClip;
_music.time = _previousTime;
_music.Play();
}
// Stack handles memory automatically
void OpenPauseMenu()
=> _music.Push(_pauseMenuMusic);
void ClosePauseMenu()
=> _music.Pop(); // returns to previous
Core Systems
AudioService
The main service. Manages 2D and 3D AudioSource pools per category, entity audio cache, and AudioBank lookup. Register it once via PrimeAudioBootstrap.Initialize() and access it through IAudioService.
You rarely call AudioService directly. Use GlobalAudio for global/UI sounds and AudioController for entity sounds. AudioService is the backend.
Inspector Config
AudioService Inspector fields
Mixer → your AudioMixer asset
Master Volume → "Master" (exposed param name in AudioMixer)
Audio Bank → your AudioBank SO (or leave empty — loads from Resources/Audio/AudioBank)
Category Groups → list of CategoryGroup entries (see below)
Pool Max Size → max 2D AudioSources per category (default 8)
Pool 3D Size → max 3D AudioSources per category (default 16)
CategoryGroup
Each entry in Category Groups defines one audio category. Add one per mixer group.
CategoryGroup fields
Category ID → AudioCategoryID asset (e.g. SFX, Music, Voice)
Mixer Group → AudioMixerGroup from your AudioMixer
Exposed Volume Param→ name of the exposed parameter in AudioMixer
Voice Limit Config → optional — max simultaneous sounds in this category
RTPC Bindings → optional — drive volume/pitch from AudioParameter
Core Systems
AudioController
Base class for all entity audio. Inherit from this instead of MonoBehaviour for any component that plays audio for an entity. Handles service resolution, per-instance clip group counters, event subscriptions, and entity cleanup.
Creating Your Controller
Minimal controller — 2D sounds
public class DoorAudioController : AudioController
{
[SerializeField] private AudioClipData openClip;
[SerializeField] private AudioClipData closeClip;
private static class Slot {
public static readonly object Open = nameof(Open);
public static readonly object Close = nameof(Close);
}
public void PlayOpen() => Play(openClip, Slot.Open);
public void PlayClose() => Play(closeClip, Slot.Close);
}
3D controller — sounds follow the entity transform
// Add IAudioEntity3DEntity to make channels follow this transform
public class CharacterAudioController
: AudioController, IAudioEntity3DEntity
{
// Required — channel GameObjects are parented here
public Transform Transform => transform;
[SerializeField] private AudioClipGroup footstepGroup;
private static class Slot {
public static readonly object Footstep = nameof(Footstep);
}
public void PlayFootstep() => Play(footstepGroup, Slot.Footstep);
}
What are Slots?
A slot is a named channel on an entity. If you call Play(clip, Slot.Walk) twice, the second call reuses the same AudioSource — the old sound is replaced. Different slots play simultaneously.
Slot behavior example
Play(footstepClip, Slot.Footstep); // ← plays on Footstep channel
Play(footstepClip, Slot.Footstep); // ← replaces previous (same slot)
Play(voiceClip, Slot.Voice); // ← plays simultaneously on Voice channel
Available Methods (inside AudioController subclass)
| Method | Description |
| Play(clip, slot, loop, fadeIn, onComplete) | Play AudioClipData on this entity |
| Play(group, slot, loop, fadeIn, onComplete) | Pick from group and play. Per-instance counter — shared SO, isolated state. |
| Play(audioID, slot, loop, fadeIn) | Play by bank ID string |
| Stop(slot, fadeOut) | Stop specific slot channel |
| StopAll(fadeOut) | Stop all channels on this entity |
| PlayDelayed(clip, delay, slot) | Play after delay seconds |
Subscribing to Events
Subscribe to events in your controller (works with any event bus)
protected override void RegisterEventListeners()
{
// Uses IAudioEventBridge — works with built-in AudioEventBus
// OR your project's IEventBus (configured in bootstrap)
EventBridge.Subscribe<PlayerLandEvent>(OnLand);
EventBridge.Subscribe<PlayerJumpEvent>(OnJump);
}
protected override void UnregisterEventListeners()
{
EventBridge.Unsubscribe<PlayerLandEvent>(OnLand);
EventBridge.Unsubscribe<PlayerJumpEvent>(OnJump);
}
private void OnLand(PlayerLandEvent e) => PlayLand();
private void OnJump(PlayerJumpEvent e) => Play(jumpClip, Slot.Jump);
Core Systems
GlobalAudio
Static facade for sounds that have no entity owner — UI, GameManager, SceneLoader. Domain-reload safe via RuntimeInitializeOnLoadMethod. Falls back to NullAudioService if not initialized.
All GlobalAudio methods
// UI / one-shots
GlobalAudio.UI("btn_click"); // by bank ID
GlobalAudio.UI(_clickClip); // by AudioClipData
GlobalAudio.UI(_clickGroup); // from AudioClipGroup
// General one-shots
GlobalAudio.Play(_explosionClip);
GlobalAudio.PlayDelayed(_clip, delay: 0.5f);
// Loops — returns handle to stop later
var h = GlobalAudio.PlayLoop(_ambientClip, fadeIn: 2f);
h.Stop(fadeOut: 1f);
// Category control
GlobalAudio.MuteCategory(_sfxCategory);
GlobalAudio.UnmuteCategory(_sfxCategory);
GlobalAudio.StopCategory(_sfxCategory, fadeOut: 0.5f);
GlobalAudio.SetVolume(_musicCategory, -6f); // in dB
GlobalAudio.SetMasterVolume(-3f);
// Mixer snapshots
GlobalAudio.TransitionToSnapshot(_underwaterSnapshot, 1f);
Init: Call PrimeAudioBootstrap.Initialize(audioService) before using GlobalAudio. If you call GlobalAudio before init, all calls go to NullAudioService — silent no-ops, no crash.
Core Systems
AudioHandle
Returned by every Play() call. Lets you control a specific sound instance after firing it. Session ID makes stale handles safe — when the pool reuses the channel, IsValid automatically becomes false.
AudioHandle usage
// Start a loop and save the handle
var h = _audio.Play(_engineClip, loop: true);
// Check state
if (h.IsPlaying) Debug.Log("Engine running");
// Change volume (with DOTween fade)
h.SetVolume(0.3f, fadeDuration: 1f);
// Pause / Resume
h.Pause();
h.Resume();
// Stop with fade-out (returns this — chainable)
h.Stop(fadeOut: 2f);
// Safe to call on Invalid — no crash
AudioHandle.Invalid.Stop(); // does nothing
| Property / Method | Type | Description |
| IsValid | bool | True while handle refers to its sound. False after pool reuse. |
| IsPlaying | bool | IsValid && source.isPlaying |
| IsPaused | bool | Channel is paused |
| ClipID | string | ID of the currently playing clip |
| Stop(fadeOut) | AudioHandle | Stop with optional DOTween fade. Chainable. |
| Pause() | AudioHandle | Pause playback |
| Resume() | AudioHandle | Resume paused playback |
| SetVolume(vol, fade) | AudioHandle | Set volume 0..1 with optional fade |
| AudioHandle.Invalid | static | Safe no-op handle. All calls do nothing. |
Core Systems
Fluent Builder
Entry via .With() extension on IAudioService. Chains options and automatically selects the correct Play overload at .Play().
Builder examples
// 3D at world position
_audio.With(_explosionClip).At(hitPoint).Play();
// Entity-bound with slot
_audio.With(_footstepGroup).For(this, Slot.Footstep).Play();
// Loop with fade-in
var h = _audio.With(_ambientClip).Loop().FadeIn(2f).Play();
// Delayed
_audio.With(_alertClip).After(0.5f).Play();
// With callback when clip ends
_audio.With(_dialogueClip)
.FadeIn(0.2f)
.OnComplete(() => subtitles.Hide())
.Play();
// Combine: entity-bound, delayed, with callback
_audio.With(_stingClip)
.For(this, Slot.Sting)
.After(0.3f)
.OnComplete(OnStingDone)
.Play();
| Method | Description |
| .With(clip / group / id) | Start builder. IAudioServiceExtensions provides this. |
| .At(Vector3) | Play at world position → routes to Play3D |
| .For(entity, slot) | Bind to entity → routes to entity Play overload |
| .Loop() | loop = true |
| .FadeIn(seconds) | DOTween fade in from 0 to clip volume |
| .After(seconds) | Delay before playing (channel reserved immediately) |
| .OnComplete(Action) | Callback when clip ends (non-looping) |
| .Play() | Terminal — executes and returns AudioHandle |
Core Systems
AudioClipData
ScriptableObject per sound. Contains everything needed to play a clip — the AudioClip itself, volume, pitch, 3D settings, priority, variation, and RTPC bindings. Create via Assets → Create → Audio → ClipData or use the Batch Generator.
| Field | Description |
| id | String key for AudioBank lookup. Used with AudioService.Play("my_id") |
| clip | The AudioClip asset |
| category | AudioCategoryID — determines which mixer group and pool |
| volume | Base volume 0..1 |
| pitch | Base pitch -3..3 |
| volumeVariation | ±variation added each Play() for natural sound |
| pitchVariation | ±variation added each Play() for natural sound |
| loop | Loop by default when played without explicit loop param |
| priority | AudioPriority — Low / Normal / High / Critical. Used for pool eviction. |
| retriggerCooldown | Min seconds before this clip can play again. Prevents machine-gun SFX. |
| is3D | Enable spatial audio (spatialBlend = 1) |
| minDistance / maxDistance | 3D rolloff distances |
| rolloffMode | Linear / Logarithmic / Custom |
| loopStartSample / loopEndSample | Custom loop points within clip |
| rtpcBindings | List of RTPCBinding — drive properties from AudioParameter |
Core Systems
AudioBank
ScriptableObject lookup table — maps string IDs to AudioClipData. Assign to AudioService in Inspector. Alternatively, place at Resources/Audio/AudioBank for auto-load.
Playing by ID (no direct SO reference needed in code)
// Register in bank: id = "explosion_large"
_audio.Play("explosion_large");
GlobalAudio.UI("btn_click");
Language bank: Call audioService.SetLanguageBank(voiceBank) to overlay a language-specific bank on top. IDs in the language bank take priority. Used for VO localization — swap bank on language change.
Music
MusicService
Dual-channel A/B crossfade music player with Push/Pop stack. Attach to AudioManager GameObject.
All MusicService methods
// Play instantly (no crossfade)
_music.Play(_menuMusic);
// Play with crossfade
_music.Play(_combatMusic, crossfadeDuration: 2f);
_music.CrossfadeTo(_combatMusic, duration: 2f); // same thing
// Stop with fade-out
_music.Stop(fadeOut: 1.5f);
// Pause / Resume (e.g. for cutscene)
_music.Pause();
_music.Resume();
// Push/Pop stack — for pause menu, loading screen, etc.
_music.Push(_pauseMusic); // saves current, crossfades to new
_music.Pop(); // crossfades back to previous
_music.ClearStack(); // empty the stack without resuming
// Get current clip
AudioClipData current = _music.GetCurrentClip();
Music
Adaptive Music — Vertical Layering
All stems (drums, bass, lead, orchestra) start at the same DSP time and play in sync forever. You only change their volumes. This is "vertical layering" — used in DOOM, NieR:Automata, Portal 2.
Setup
1. Export all stems from your DAW at the same length, starting from beat 1.
2. Add AdaptiveMusicSystem to AudioManager.
3. Add one MusicStem entry per audio track.
4. Add MusicState entries — one per game state — with target volumes per stem.
Usage
// Start — all stems sync at same DSP time
_adaptive.Play("Exploration"); // drums only
// Transition — stems fade to new volumes
_adaptive.TransitionTo("Combat", duration: 3f); // +bass +lead
_adaptive.TransitionTo("Boss", duration: 1f); // full orchestra
// Stop all stems
_adaptive.Stop(fadeOut: 2f);
MusicState config in Inspector
State Name: "Combat"
Stem Volumes: [1.0, 1.0, 0.8, 0.0]
Drums Bass Lead Orch
// Index matches the Stems list order
Music
AudioPlaylist
Ordered or shuffled queue of music tracks. Attach to AudioManager or a dedicated MusicPlayer GameObject.
Usage
_playlist.Play(); // start
_playlist.Next(); // skip to next track
_playlist.Previous(); // go back
_playlist.JumpTo(2); // jump to index 2
_playlist.Stop(fadeOut: 1f); // stop
// Swap playlist at runtime (e.g. enter boss arena)
_playlist.SetTracks(bossPlaylist, restart: true);
| Inspector Field | Description |
| Tracks | List of AudioClipData — the music queue |
| Mode | Sequential, Random, RandomNoRepeat |
| Crossfade Duration | Seconds for A/B crossfade between tracks |
| Auto Advance | Automatically move to next track when current ends |
| Loop | Loop the playlist after last track |
| Play On Awake | Start on scene load |
Music
Stinger System
Short musical cues that punch in over current music. Three modes:
| Mode | Behavior | Use case |
| Overlay | Plays on top of music simultaneously | Kill confirmed hit, score UI |
| DuckAndPlay | Ducks music, plays stinger, restores volume | Cinematic hit, boss phase reveal |
| CutAndReturn | Fades out music, plays stinger, crossfades back | Level complete fanfare, game over |
Usage
_stingers.Play(_killConfirmedClip, StingerMode.Overlay);
_stingers.Play(_levelCompleteClip, StingerMode.CutAndReturn);
_stingers.Play(_dangerStingClip, StingerMode.DuckAndPlay, duckAmount: -10f);
Advanced
Surface Audio
Footstep, land, and impact sounds automatically switch based on the ground material under the character.
Setup Steps
1
Create AudioSurface assets
One per material type. Assets → Create → Audio → Surface
GrassSurface.asset → footstepGroup, landGroup, impactGroup
MetalSurface.asset → footstepGroup, landGroup, impactGroup
WoodSurface.asset → ...
2
Create SurfaceAudioDatabase
Assets → Create → Audio → SurfaceDatabase. Map PhysicsMaterials, tags, and TerrainLayers to AudioSurface assets.
By Material: GrassMat → GrassSurface
By Tag: "Metal" → MetalSurface
By Terrain: GrassLayer → GrassSurface
3
Add SurfaceDetector to character
Attach SurfaceDetector component. Assign the SurfaceAudioDatabase.
SurfaceDetector
Database → SurfaceAudioDatabase
Ray Length → 0.3 (distance to check below feet)
Recalc Dist → 0.5 (re-detect after moving 0.5m)
4
Use in CharacterAudioController
public void PlayFootstep()
{
var surface = _surfaceDetector.CurrentSurface;
var group = surface?.footstepGroup ?? _defaultFootsteps;
Play(group, Slot.Footstep);
}
Adding a new material: open SurfaceAudioDatabase, add one entry, assign clips. No code changes.
Advanced
RTPC — Real-Time Parameters
Drive audio properties (volume, pitch, low-pass cutoff, spatial blend) from gameplay values via an AnimationCurve. No code needed after setup — just set the parameter value.
Setup
1. Create AudioParameter SO
Assets → Create → Audio → Parameter (RTPC)
Name: "EngineRPM"
Min Value: 0
Max Value: 8000
Default: 0
2. Add RTPCBinding to AudioClipData
AudioClipData → RTPC Bindings → Add entry
Parameter: EngineRPM
Target: Pitch
Curve: (0, 0.6) → (1, 1.8) // RPM 0→8000 maps pitch 0.6→1.8
3. Drive from gameplay — one line
[SerializeField] private AudioParameter _engineRPM;
void Update() => _engineRPM.Value = currentRPM;
Available RTPC Targets
| Target | Controls | Example use |
| Volume | AudioSource.volume | Heartbeat volume tied to health |
| Pitch | AudioSource.pitch | Engine pitch tied to RPM/speed |
| LowPassCutoff | AudioLowPassFilter.cutoffFrequency | Muffle sound underwater based on depth |
| SpatialBlend | AudioSource.spatialBlend (0=2D, 1=3D) | Blend 2D↔3D based on distance from wall |
Advanced
VoiceOver Queue
Manages dialogue playback order. Attach to AudioManager or a dedicated VoiceManager.
Usage
// Queue lines — play sequentially
_voQueue.Play(_greetingClip, policy: VoicePolicy.Queue);
_voQueue.Play(_questInfoClip, policy: VoicePolicy.Queue);
// Interrupt — cut current, play now
_voQueue.Play(_urgentClip, policy: VoicePolicy.Interrupt);
// Discard — if something is playing, skip
_voQueue.Play(_barkClip, policy: VoicePolicy.Discard);
// Subtitle hooks
_voQueue.OnLineStart += line => subtitles.Show(line.subtitleKey);
_voQueue.OnLineEnd += line => subtitles.Hide();
// Clear queue
_voQueue.ClearQueue();
_voQueue.StopCurrent(fadeOut: 0.3f);
| Policy | Behavior |
| Queue | Add to end — plays when previous lines finish |
| Interrupt | Stop current immediately, play now, clear queue |
| Discard | If anything is playing or queued, silently ignore |
Advanced
Auto Ducking
Automatically reduces the volume of one category when another plays. Configured entirely in Inspector via DuckingSystem component. No code needed.
DuckingRule config in Inspector
Trigger Category: Voice ← when this category plays
Target Category: Music ← reduce this category
Duck Amount: -12 dB ← by this many dB
Duck In: 0.1s ← how fast to duck
Duck Out: 0.5s ← how fast to recover after VO ends
Hold Duration: 0s ← hold duck for this long before recovering
Hearing simulation: Set Trigger = SFX, Target = Everything, Duck Amount = -3dB, Duck In = 0.02s, Duck Out = 0.3s, Hold = 0.1s — simulates ear compression after loud sounds.
Advanced
Audio Zones
No-code ambient audio. Attach AudioZone to any GameObject with a Collider. The collider must be set to Is Trigger = true.
AudioZone Inspector config
Clip: ForestAmbient.asset ← ambient loop
Group: (optional) pick random clip from group
Enter Stinger: (optional) one-shot when player enters
Exit Stinger: (optional) one-shot when player exits
Fade In: 1.5s
Fade Out: 2.0s
Use Distance Fade: true
Volume Curve: AnimationCurve ← X=0 center, X=1 edge. Y=volume
Player Tag: "Player"
Distance fade: Use the Volume Curve to make sound louder at zone center and quieter near edges — seamless blend when entering/leaving.
Advanced
Audio LOD
Distance-based culling — don't even request a pool channel if the source is too far to hear. Prevents pool exhaustion on crowd scenes.
Usage — call before Play3D in custom code
// AudioService calls this automatically for all 3D sounds
// But you can use it manually too:
if (!AudioLODSystem.CanHear(transform.position, _clip))
return; // too far — skip
// Get priority based on distance automatically
var priority = AudioLODSystem.GetDistancePriority(
transform.position, _clip.maxDistance);
// Critical <10% dist, High <35%, Normal <70%, Low otherwise
Setup: PrimeAudioBootstrap.Initialize() automatically caches the AudioListener for LOD. If you swap cameras at runtime, call AudioLODSystem.SetListener(newListener.transform).
Advanced
Runtime Audio Effects
Apply Unity's built-in audio filters at runtime to an AudioHandle.
Usage
var h = _audio.With(_voiceClip).Play();
// Muffle through a wall
AudioRuntimeEffect.ApplyLowPass(h, cutoffFrequency: 800f);
// Cave / large hall reverb
AudioRuntimeEffect.ApplyReverb(h, AudioReverbPreset.Cave);
// Radio / intercom effect
AudioRuntimeEffect.ApplyHighPass(h, cutoffFrequency: 2000f);
// Remove all effects
AudioRuntimeEffect.ClearAllEffects(h);
// Remove specific effect
AudioRuntimeEffect.RemoveLowPass(h);
Integration
Bootstrap & Extension Points
One call wires everything. All extensions are optional — system works out of the box with zero config.
Minimal — no custom integrations
PrimeAudioBootstrap.Initialize(audioService);
Full — with all custom integrations
PrimeAudioBootstrap.Initialize(
audioService: _audioService,
clipLoader: new AddressableAudioClipLoader(),
eventBridge: new MyEventBridge(_eventBus)
);
IAudioClipLoader
How AudioClips are loaded. Swap to enable Addressables or any async loader.
Default: DirectAudioClipLoader
IAudioEventBridge
How audio events are published. Swap to use your project's IEventBus.
Default: BuiltinAudioEventBridge
ISurfaceProvider
How ground material is detected. Swap to use custom logic.
Default: SurfaceDetector (raycast)
Integration
Addressables
1
Install package
Window → Package Manager → Add by name: com.unity.addressables
2
Add scripting symbol
Edit → Project Settings → Player → Scripting Define Symbols
Add: PRIME_AUDIO_ADDRESSABLES → Apply
3
Set clip loader in bootstrap
PrimeAudioBootstrap.Initialize(audioService,
clipLoader: new AddressableAudioClipLoader());
4
Add addressable key to AudioClipData
Add a field manually (or use a partial class) and mark your AudioClips as Addressable in the Unity Editor.
public string addressableKey; // "Audio/Footstep_Grass"
Integration
Custom Event Bus
Write a 3-method adapter. Call it once in bootstrap. All EventBridge.Subscribe/Unsubscribe calls in your AudioControllers will automatically route through your bus.
Adapter — implement IAudioEventBridge
public class MyEventBridge : IAudioEventBridge
{
private readonly IEventBus _bus;
public MyEventBridge(IEventBus bus) => _bus = bus;
public void Subscribe<T>(Action<T> h) => _bus.Subscribe(h);
public void Unsubscribe<T>(Action<T> h) => _bus.Unsubscribe(h);
public void Publish<T>(T e) => _bus.Publish(e);
}
Built-in Events
These structs are available out of the box in AudioEventBus.cs. Publish from gameplay, subscribe in AudioControllers.
Publish from gameplay
AudioEventBus.Publish(new PlayerFootstepEvent { position = pos });
AudioEventBus.Publish(new PlayerLandEvent { fallVelocity = vel });
AudioEventBus.Publish(new WeaponFireEvent { weaponID = "pistol" });
AudioEventBus.Publish(new GamePausedEvent { });
Editor Tools
AudioClipData Generator
Batch-generates AudioClipData ScriptableObjects from raw AudioClip assets. Open via Tools → Prime Audio → AudioClipData Generator.
Also available via right-click: Assets → Create → Audio → AudioClipData from Selection
Workflow
1
Set Source
Select a folder (with optional "Include Subfolders") or switch to "Current Selection" mode and select AudioClips in the Project window.
2
Set Output Folder & Bank
Choose where to save the generated assets. Optionally pick an AudioBank to add them to automatically.
3
Review Preview Table
The table shows every clip with auto-detected ID, category and 3D flag. Edit any ID directly in the table. Yellow = will overwrite, grey = will skip.
Smart Category: folder "sfx/" → category SFX
folder "ui/" → category UI
folder "voice/" → category Voice
folder "music/" → category Music
Smart 3D: path has "3d", "spatial", "ambient", "world" → is3D = true
ID: clip name lowercased and spaces → underscores
4
Generate
Click Generate. Assets are created and optionally added to AudioBank. Existing assets are skipped unless Overwrite is enabled.
Editor Tools
AudioBank Inspector
Custom editor for AudioBank ScriptableObjects. Select any AudioBank asset to see it.
| Feature | Description |
| Search bar | Filter by ID or clip name in real time |
| Missing only filter | Show only entries where the AudioClip reference is null — find broken refs before runtime |
| ▶ Play button | Preview any clip audio directly in Inspector without entering Play Mode |
| ⊙ Ping button | Highlight the AudioClipData asset in Project window |
| Drag & drop zone | Drag AudioClipData assets directly onto the Inspector to add them to the bank |
| Sort A-Z | Sort all entries alphabetically by ID |
| Remove Nulls | Remove all null entries in one click |
| Stats header | Shows total count, missing clips count, duplicate IDs count |
API Reference
IAudioService
| Method | Returns | Description |
| Play(string id, loop, fadeIn) | AudioHandle | Play by string ID from AudioBank |
| Play(AudioClipData, loop, fadeIn) | AudioHandle | Play direct clip reference |
| Play(AudioClipGroup, loop, fadeIn) | AudioHandle | Pick from group and play |
| Play(entity, clip, slot, loop, fadeIn, onComplete) | AudioHandle | Entity-bound. 3D channels follow transform if IAudioEntity3DEntity. |
| Play(entity, group, slot, ...) | AudioHandle | Pick from group for entity |
| Play3D(Vector3, clip, loop, fadeIn) | AudioHandle | Positional sound from 3D pool at world position |
| PlayDelayed(clip, delay, loop) | AudioHandle | Reserve channel now, play after delay seconds |
| PlayDelayed(entity, clip, delay, slot) | AudioHandle | Entity-bound delayed play |
| Stop(entity, slot, fadeOut) | void | Stop specific entity slot |
| StopAll(entity, fadeOut) | void | Stop all channels for entity |
| RemoveEntity(entity) | void | Destroy all channels for entity. Called automatically by AudioController on Destroy. |
| StopCategory(categoryID, fadeOut) | void | Stop all sounds in a category pool |
| SetMasterVolume(db) | void | Set AudioMixer master parameter |
| SetVolume(categoryID, db) | void | Set exposed mixer param for category |
| MuteGroup(categoryID) | void | SetVolume to -80dB |
| UnmuteGroup(categoryID) | void | SetVolume to 0dB |
| TransitionToSnapshot(snapshot, duration) | void | AudioMixer snapshot transition |
| SetLanguageBank(bank) | void | Overlay language VO bank. IDs in this bank take priority over main bank. |
| Bank | AudioBank | The assigned AudioBank |
API Reference
AudioHandle
| Member | Type | Description |
| IsValid | bool | True while handle still refers to its sound. Session ID auto-invalidates on channel reuse. |
| IsPlaying | bool | IsValid && AudioSource.isPlaying |
| IsPaused | bool | Channel is currently paused |
| ClipID | string | ID of the currently playing clip (from AudioClipData.id) |
| Stop(fadeOut) | AudioHandle | Stop with optional DOTween fade. Chainable. |
| Pause() | AudioHandle | Pause AudioSource. Chainable. |
| Resume() | AudioHandle | UnPause AudioSource. Chainable. |
| SetVolume(vol, fade) | AudioHandle | Volume 0..1 with optional fade seconds. Chainable. |
| AudioHandle.Invalid | static | Safe no-op instance. All methods silently do nothing. |
API Reference
Fluent Builder
Access via _audio.With(clip/group/id). Provided by IAudioServiceExtensions.
| Method | Description |
| .With(clip / group / id) | Start the builder chain |
| .At(Vector3) | Set world position → routes to Play3D at .Play() |
| .For(entity, slot) | Bind to entity → routes to entity Play overload at .Play() |
| .Loop() | Set loop = true |
| .FadeIn(seconds) | DOTween fade from 0 to clip volume on start |
| .After(seconds) | Delay — channel reserved now, plays after delay |
| .OnComplete(Action) | Fires when non-looping clip ends naturally |
| .Play() | Terminal — executes the play and returns AudioHandle |