Audio
Music, sound effects, and voice through a Unity AudioMixer with pooled sources and pause-aware fades.
Music, effects, and voice through three AudioMixer channels · Pooled sources, automatic fades, pause-aware behavior · No-op replacement:
NullAudio
CGS routes all in-game sound through IAudioService — three dedicated channels on a Unity AudioMixer: Music (looping background tracks), Sfx (one-shot sound effects, 2D or positioned in 3D), and Voice (dialogue and announcements, one clip at a time). Pooled AudioSource components mean no garbage-collection spikes when effects fire rapidly; fades and crossfades are built in; and pause behaves the way players expect — music keeps playing while gameplay effects stop. No network calls, no third-party plugins, IL2CPP-safe.
What it does
- Music with automatic fades.
PlayMusic(clip)fades the track in; call it again with a different clip and the service crossfades instead of cutting.StopMusic()fades out. - Pooled sound effects.
PlaySfx(clip)grabs a source from a pre-allocated pool, applies a slight random pitch variation (about ±10% by default) so repeated effects sound natural, and returns the source to the pool when the clip ends. When every source is busy, the oldest playing effect is stopped to make room. - One-at-a-time voice.
PlayVoice(clip)stops whatever voice line was playing and starts the new one — dialogue never overlaps itself. - Mixer snapshots. A snapshot is a saved set of mixer volume levels. Transition between presets — normal gameplay, music ducked under dialogue, muffled behind a menu — with one call and a smooth fade.
- Live volume settings. Volume sliders wired through the Configuration service reach the mixer in real time.
Getting the service
Resolve in Awake (on a class marked [DefaultExecutionOrder(100)]) and cache:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class SoundManager : MonoBehaviour
{
private IAudioService _audio;
private void Awake()
{
_audio = ServiceLocator.Resolve<IAudioService>();
}
}
Do not call Resolve inside Update() — the lookup is a dictionary search. Cache it once.
API reference
Background music
void PlayMusic(AudioClip clip, float fadeInSeconds = -1f, float crossfadeSeconds = -1f)
Starts a looping track with a fade-in. If a track is already playing, the service crossfades to the new one over crossfadeSeconds instead. Pass -1f to use the defaults (1.5-second fade-in, 1.0-second crossfade); pass 0f for a hard cut.
void StopMusic(float fadeOutSeconds = -1f) // Fade out and stop (default 2.0 seconds)
bool IsMusicPlaying { get; } // true while a track is playing
Sound effects (2D and 3D)
bool PlaySfx(AudioClip clip, float pitchVariation = -1f)
bool PlaySfx(AudioClip clip, Vector3 worldPosition, float pitchVariation = -1f)
The first overload plays a flat 2D effect; the second plays a 3D effect at a world position with distance-based rolloff. Both return true on success, or false when the clip is null, not yet loaded, or the pool rejected the request. pitchVariation is the random pitch half-range: -1f uses the default (about ±10%), 0f disables variation, and values above 0.5f are clamped with a warning.
int ActiveSfxCount { get; } // Number of effect sources currently playing
Voice and dialogue
void PlayVoice(AudioClip clip) // Starts the clip; any prior voice clip stops immediately
void StopVoice() // Stops the current voice clip
bool IsVoicePlaying { get; } // true while voice is playing
Mixer snapshots and global stop
void SetSnapshot(AudioSnapshot snapshot, float transitionSeconds = -1f)
void SetSnapshot(string snapshotName, float transitionSeconds = -1f)
void StopAll()
The enum overload transitions to one of the built-in presets — Normal, DialogueDucked (music and effects lowered about 6 dB under dialogue), or MenuOpen. The string overload targets any snapshot you added to your own mixer. StopAll() silences every channel immediately, with no fade.
Full example
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class GameAudio : MonoBehaviour
{
[SerializeField] private AudioClip _explosionClip;
private IAudioService _audio;
private void Awake()
{
_audio = ServiceLocator.Resolve<IAudioService>();
}
public void PlayBattleMusic(AudioClip clip)
{
// Fade in over 1.5 seconds; if music is already playing,
// the service crossfades to the new track automatically.
_audio.PlayMusic(clip, fadeInSeconds: 1.5f);
}
public void PlayExplosion(Vector3 position)
{
// 3D effect with the default pitch variation (about ±10%)
_audio.PlaySfx(_explosionClip, position);
}
public void DialogueStart()
{
// Duck music and effects while dialogue plays
_audio.SetSnapshot(AudioSnapshot.DialogueDucked);
}
public void PlayDialogue(AudioClip voiceClip)
{
_audio.PlayVoice(voiceClip);
}
public void DialogueEnd()
{
_audio.SetSnapshot(AudioSnapshot.Normal);
}
}
Turning it off
ServiceLocator.Replace<IAudioService>(new NullAudio());
This one line silences the service: every method becomes a no-op and no game code changes. Useful for tests, voice-over recording sessions, or when you bring your own audio middleware. The AudioListener is unaffected — your camera still needs one, but nothing plays through it.
Common pitfalls
-
Main thread only. All
PlayMusic,PlaySfx,PlayVoice, andSetSnapshotcalls must happen on the main thread. If a worker thread needs to trigger sound, marshal the call to the main thread first — the Scheduler can do this for you. -
Pause behavior is built in. Music fades run on the Time service's Background clock, so music keeps playing when gameplay pauses. Effect timers run on the Gameplay clock, so effects stop. "Music continues, effects stop" needs no extra code from you.
-
Prewarm the pool before busy scenes. By default, 8 effect sources are pre-allocated at startup. If your first combat scene fires 20 explosions at once, the pool grows on demand and can cause a frame hitch. Raise
AudioOptions.sfxPoolPrewarmCountat startup to pre-allocate more — a little startup memory for zero runtime stutter. -
Volume sliders need a flush to apply. The Configuration service batches setting writes, so calling
Set(new AudioSettings { ... })alone only queues the change — the mixer hears nothing yet. CallFlushPending<AudioSettings>()on your resolvedIConfigurationright afterSetfor real-time slider feedback. Once flushed, the mixer updates immediately. -
Custom AudioMixer parameter names. If you supply your own
AudioMixerthroughAudioOptions, it must expose four parameters:MasterVolume,MusicVolume,SfxVolume, andVoiceVolume(all in dB). If your names differ, point the service at them viaAudioOptions.masterParam,musicParam,sfxParam, andvoiceParam. -
Clip length drives effect cleanup. After
PlaySfx(clip), the source returns to the pool automatically afterclip.lengthseconds, measured in unscaled time — slow motion does not delay cleanup. No manual bookkeeping needed. -
Custom types need
link.xmlfor IL2CPP. If you add your own configuration types that hold audio settings, add alink.xmlentry so IL2CPP does not strip them:<assembly fullname="Assembly-CSharp"> <type fullname="YourGame.CustomAudioConfig" preserve="all" /> </assembly>
Related pages
- Time — the clocks behind pause-aware fades
- Configuration — settings groups and
FlushPending - Scheduler — marshaling calls to the main thread