Prime Audio System
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
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
// 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)
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
[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
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
// 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)
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
// 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
Standard Unity
Prime Audio System
// 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
Unity vs Prime Audio
Surface-Based Footsteps
Footstep sound that changes by ground material
Standard Unity
Prime Audio System
// Manual raycast + if/switch per material void PlayFootstep() { if (!Physics.Raycast(transform.position + Vector3.up * 0.1f, Vector3.down, out var hit, 0.3f)) return; var mat = hit.collider.sharedMaterial; if (mat == grassMat) _src.PlayOneShot(_grassClips[Random.Range( 0, _grassClips.Length)]); else if (mat == metalMat) _src.PlayOneShot(_metalClips[Random.Range( 0, _metalClips.Length)]); // ... repeat for every material }
// SurfaceDetector does raycast + lookup // SurfaceAudioDatabase maps mat → group void PlayFootstep() { var surface = _surfaceDetector.CurrentSurface; var group = surface?.footstepGroup ?? _defaultFootsteps; Play(group, Slot.Footstep); } // Add new material: open SurfaceDB, // add entry. Zero code changes.
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)

MethodDescription
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 / MethodTypeDescription
IsValidboolTrue while handle refers to its sound. False after pool reuse.
IsPlayingboolIsValid && source.isPlaying
IsPausedboolChannel is paused
ClipIDstringID of the currently playing clip
Stop(fadeOut)AudioHandleStop with optional DOTween fade. Chainable.
Pause()AudioHandlePause playback
Resume()AudioHandleResume paused playback
SetVolume(vol, fade)AudioHandleSet volume 0..1 with optional fade
AudioHandle.InvalidstaticSafe 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();
MethodDescription
.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.

FieldDescription
idString key for AudioBank lookup. Used with AudioService.Play("my_id")
clipThe AudioClip asset
categoryAudioCategoryID — determines which mixer group and pool
volumeBase volume 0..1
pitchBase pitch -3..3
volumeVariation±variation added each Play() for natural sound
pitchVariation±variation added each Play() for natural sound
loopLoop by default when played without explicit loop param
priorityAudioPriority — Low / Normal / High / Critical. Used for pool eviction.
retriggerCooldownMin seconds before this clip can play again. Prevents machine-gun SFX.
is3DEnable spatial audio (spatialBlend = 1)
minDistance / maxDistance3D rolloff distances
rolloffModeLinear / Logarithmic / Custom
loopStartSample / loopEndSampleCustom loop points within clip
rtpcBindingsList 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 FieldDescription
TracksList of AudioClipData — the music queue
ModeSequential, Random, RandomNoRepeat
Crossfade DurationSeconds for A/B crossfade between tracks
Auto AdvanceAutomatically move to next track when current ends
LoopLoop the playlist after last track
Play On AwakeStart on scene load
Music
Stinger System

Short musical cues that punch in over current music. Three modes:

ModeBehaviorUse case
OverlayPlays on top of music simultaneouslyKill confirmed hit, score UI
DuckAndPlayDucks music, plays stinger, restores volumeCinematic hit, boss phase reveal
CutAndReturnFades out music, plays stinger, crossfades backLevel 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

TargetControlsExample use
VolumeAudioSource.volumeHeartbeat volume tied to health
PitchAudioSource.pitchEngine pitch tied to RPM/speed
LowPassCutoffAudioLowPassFilter.cutoffFrequencyMuffle sound underwater based on depth
SpatialBlendAudioSource.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);
PolicyBehavior
QueueAdd to end — plays when previous lines finish
InterruptStop current immediately, play now, clear queue
DiscardIf 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.

FeatureDescription
Search barFilter by ID or clip name in real time
Missing only filterShow only entries where the AudioClip reference is null — find broken refs before runtime
▶ Play buttonPreview any clip audio directly in Inspector without entering Play Mode
⊙ Ping buttonHighlight the AudioClipData asset in Project window
Drag & drop zoneDrag AudioClipData assets directly onto the Inspector to add them to the bank
Sort A-ZSort all entries alphabetically by ID
Remove NullsRemove all null entries in one click
Stats headerShows total count, missing clips count, duplicate IDs count
API Reference
IAudioService
MethodReturnsDescription
Play(string id, loop, fadeIn)AudioHandlePlay by string ID from AudioBank
Play(AudioClipData, loop, fadeIn)AudioHandlePlay direct clip reference
Play(AudioClipGroup, loop, fadeIn)AudioHandlePick from group and play
Play(entity, clip, slot, loop, fadeIn, onComplete)AudioHandleEntity-bound. 3D channels follow transform if IAudioEntity3DEntity.
Play(entity, group, slot, ...)AudioHandlePick from group for entity
Play3D(Vector3, clip, loop, fadeIn)AudioHandlePositional sound from 3D pool at world position
PlayDelayed(clip, delay, loop)AudioHandleReserve channel now, play after delay seconds
PlayDelayed(entity, clip, delay, slot)AudioHandleEntity-bound delayed play
Stop(entity, slot, fadeOut)voidStop specific entity slot
StopAll(entity, fadeOut)voidStop all channels for entity
RemoveEntity(entity)voidDestroy all channels for entity. Called automatically by AudioController on Destroy.
StopCategory(categoryID, fadeOut)voidStop all sounds in a category pool
SetMasterVolume(db)voidSet AudioMixer master parameter
SetVolume(categoryID, db)voidSet exposed mixer param for category
MuteGroup(categoryID)voidSetVolume to -80dB
UnmuteGroup(categoryID)voidSetVolume to 0dB
TransitionToSnapshot(snapshot, duration)voidAudioMixer snapshot transition
SetLanguageBank(bank)voidOverlay language VO bank. IDs in this bank take priority over main bank.
BankAudioBankThe assigned AudioBank
API Reference
AudioHandle
MemberTypeDescription
IsValidboolTrue while handle still refers to its sound. Session ID auto-invalidates on channel reuse.
IsPlayingboolIsValid && AudioSource.isPlaying
IsPausedboolChannel is currently paused
ClipIDstringID of the currently playing clip (from AudioClipData.id)
Stop(fadeOut)AudioHandleStop with optional DOTween fade. Chainable.
Pause()AudioHandlePause AudioSource. Chainable.
Resume()AudioHandleUnPause AudioSource. Chainable.
SetVolume(vol, fade)AudioHandleVolume 0..1 with optional fade seconds. Chainable.
AudioHandle.InvalidstaticSafe no-op instance. All methods silently do nothing.
API Reference
Fluent Builder

Access via _audio.With(clip/group/id). Provided by IAudioServiceExtensions.

MethodDescription
.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