This package provides a lean sound manager for a Unity project.
- Play/Pause/Resume/Fade/Stop individual or all sounds
- Set/Mute/Fade volume of Audio Mixers
- Efficiently uses object pooling under the hood
- Access anywhere from code (as persistent singleton)
- Async/Await support (including UniTask!)
- Sound Emitter & Sound Volume Mixer components for non-coders
Through the Package Manager in the Editor as a git package: https://github.com/devolfer/devolfer-sound.git
.
The Package Manager can be opened under Window -> Package Manager
.
Or as "com.devolfer.sound": "https://github.com/devolfer/devolfer-sound.git"
in Packages/manifest.json
.
Manual import into a folder is of course also possible.
Even if async/await workflow is not intended to be used, it is very favourable to install UniTask anyway.
Synchronous methods will be invoked as allocation-free tasks under the hood!
The installation guide can be found in the Official UniTask Repo.
Once installed, all code of this package will automatically compile using UniTask
instead of standard C# Task
!
This will potentially break any existing asynchronous code usage, that was initially expecting a C# Task return value.
Using code hints is highly encouraged and can be enough to get a grasp of this package.
To see them in an IDE, generating .csproj files for git packages must be enabled.
This can be done by going to Preferences|Settings -> External Tools
, marking the checkbox and regenerating the project files.
If in doubt, the following sections aim to provide as clear explanations and examples as possible.
Playing a sound is as simple as calling the Play
method and passing an AudioClip
to it.
using Devolfer.Sound;
using UnityEngine;
public class YourBehaviour : MonoBehaviour
{
// Injects clip via Editor Inspector
[SerializeField] private AudioClip audioClip;
private void YourMethod()
{
// Plays clip through the SoundManager instance
SoundManager.Instance.Play(audioClip);
// *** There is no need for an AudioSource component.
// The SoundManager will get a SoundEntity instance from its pool,
// play the clip through it, and then return it back to the pool. ***
}
}
To alter the behaviour, there are various optional parameters that can be passed to the Play
method.
// Plays clip at world position
SoundManager.Instance.Play(audioClip, position: new Vector3(0, 4, 2));
// *** The above call is very similar to Unitys 'AudioSource.PlayClipAtPoint()' method,
// however, there is no overhead of instantiating & destroying a GameObject involved! ***
// Injects follow target transform via Editor Inspector
[SerializeField] private Transform transformToFollow;
// Plays clip at local position while following 'transformToFollow'
SoundManager.Instance.Play(audioClip, followTarget: transformToFollow, position: new Vector3(0, 4, 2));
// Plays clip with fade in of 1 second and applies InSine easing
SoundManager.Instance.Play(audioClip, fadeIn: true, fadeInDuration: 1f, fadeInEase = Ease.InSine);
// Plays clip and prints log statement at completion
SoundManager.Instance.Play(audioClip, onComplete: () => Debug.Log("Yeah, this sound finished playing!"));
For any further custom sound settings, there is the SoundProperties
class.
It mimics the public properties of an AudioSource
and allows control over e.g. volume & pitch.
// Defines random volume
float volume = UnityEngine.Random.Range(.5f, 1f);
// Defines random pitch using pentatonic scale
float pitch = 1f;
int[] pentatonicSemitones = { 0, 2, 4, 7, 9 };
int amount = pentatonicSemitones[UnityEngine.Random.Range(0, pentatonicSemitones.Length)];
for (int i = 0; i < amount; i++) pitch *= 1.059463f;
// Plays via SoundProperties with clip, volume & pitch
SoundManager.Instance.Play(new SoundProperties(audioClip) { Volume = volume, Pitch = pitch });
// *** Passing new SoundProperties like above is just for demonstration.
// When possible those should be cached & reused! ***
It is also no problem to pass an AudioSource
directly.
// Injects AudioSource via Editor Inspector
[SerializeField] private AudioSource audioSource;
// Plays with 'audioSource' properties
SoundManager.Instance.Play(audioSource);
// Plays with 'audioSource' properties, but this time looped
SoundManager.Instance.Play(new SoundProperties(audioSource) { Loop = true });
// *** The call above passes an implicit SoundProperties copy of the AudioSource properties.
// This can be useful for selectively changing AudioSource properties at call of Play. ***
Playing a sound async can be done by calling the PlayAsync
method.
Its declaration looks very similar to all the above.
private async void YourAsyncMethod()
{
CancellationToken someCancellationToken = new();
try
{
// Plays clip with default fade in
await SoundManager.Instance.PlayAsync(audioClip, fadeIn: true);
// Plays clip with cancellation token 'someCancellationToken'
await SoundManager.Instance.PlayAsync(audioClip, cancellationToken: someCancellationToken);
// *** Using tokens is optional, as each playing SoundEntity handles
// its own cancellation when needed. ***
// Plays clip with SoundProperties at half volume & passes 'someCancellationToken'
await SoundManager.Instance.PlayAsync(
new SoundProperties(audioClip) { Volume = .5f },
cancellationToken: someCancellationToken);
// Plays with 'audioSource' properties
await SoundManager.Instance.PlayAsync(audioSource);
Debug.Log("Awaiting is done. All sounds have finished playing one after another!");
}
catch (OperationCanceledException _)
{
// Handle cancelling however needed
}
}
Pausing and resuming an individual sound requires to pass a SoundEntity
or an AudioSource
.
The Play
method returns a SoundEntity
, PlayAsync
optionally outs a SoundEntity
.
// Plays clip & caches playing SoundEntity into variable 'soundEntity'
SoundEntity soundEntity = SoundManager.Instance.Play(audioClip);
// Plays clip async & outs playing SoundEntity into variable 'soundEntity'
await SoundManager.Instance.PlayAsync(out SoundEntity soundEntity, audioClip);
// Doing the above with 'audioSource' properties
SoundManager.Instance.Play(audioSource);
await SoundManager.Instance.PlayAsync(audioSource);
// *** When calling Play with an AudioSource it is not mandatory to cache the playing SoundEntity.
// The SoundManager will cache both in a Dictionary entry for later easy access! ***
Calling Pause
and Resume
can then be called on any playing sound.
// Pauses & Resumes cached/outed 'soundEntity'
SoundManager.Instance.Pause(soundEntity);
SoundManager.Instance.Resume(soundEntity);
// Pauses & Resumes via original `audioSource`
SoundManager.Instance.Pause(audioSource);
SoundManager.Instance.Resume(audioSource);
// Pauses & Resumes all sounds
SoundManager.Instance.PauseAll();
SoundManager.Instance.ResumeAll();
// *** A sound, that is in the process of stopping, cannot be paused! ***
Stopping also requires to pass a SoundEntity
or an AudioSource
.
// Stops both cached 'soundEntity' & 'audioSource'
SoundManager.Instance.Stop(soundEntity);
SoundManager.Instance.Stop(audioSource);
// Same as above as async call
await SoundManager.Instance.StopAsync(soundEntity);
await SoundManager.Instance.StopAsync(audioSource);
// Stops all sounds
SoundManager.Instance.StopAll();
await SoundManager.Instance.StopAllAsync();
By default, the Stop
and StopAsync
methods fade out when stopping. This can be individually set.
// Stops cached 'soundEntity' with long fadeOut duration
SoundManager.Instance.Stop(
soundEntity,
fadeOutDuration: 3f,
fadeOutEase: Ease.OutSine,
onComplete: () => Debug.Log("Stopped sound after long fade out."));
// Stops cached 'soundEntity' with no fade out
SoundManager.Instance.Stop(soundEntity, fadeOut: false);
// Stops 'audioSource' async with default fade out
await SoundManager.Instance.StopAsync(audioSource, cancellationToken: someCancellationToken);
For fading a sound, it is mandatory to set a targetVolume
and duration
.
// Fades cached 'soundEntity' to volume 0.2 over 1 second
SoundManager.Instance.Fade(soundEntity, .2f, 1f);
// Pauses cached 'soundEntity' & then fades it to full volume with InExpo easing over 0.5 seconds
SoundManager.Instance.Pause(soundEntity);
SoundManager.Instance.Fade(
soundEntity,
1f,
.5f,
ease: Ease.InExpo,
onComplete: () => Debug.Log("Quickly faded in paused sound again!"));
// Fades 'audioSource' to volume 0.5 with default ease over 2 seconds
await SoundManager.Instance.FadeAsync(audioSource, .5f, 2f, cancellationToken: someCancellationToken);
// *** Stopping sounds cannot be faded and paused sounds will automatically resume when faded! ***
The CrossFade
and CrossFadeAsync
methods provide ways to simultaneously fade two sounds out and in.
This means, an existing sound will be stopped fading out, while a new one will play fading in.
// Cross-fades cached 'soundEntity' & new clip over 1 second
SoundEntity newSoundEntity = SoundManager.Instance.CrossFade(1f, soundEntity, new SoundProperties(audioClip));
// Async cross-fades two sound entities & outs the new one
await SoundManager.Instance.CrossFadeAsync(out newSoundEntity, 1f, soundEntity, new SoundProperties(audioClip));
// *** The returned SoundEntity will be the newly playing one
// and it will always fade in to full volume. ***
Simplified cross-fading might not lead to the desired outcome.
If so, invoking two Fade
calls simultaneously will grant finer fading control.
This section can be skipped, if the AudioMixerDefault
asset included in this package suffices.
It consists of the groups Master
, Music
and SFX
, with respective Exposed Parameters
: VolumeMaster
, VolumeMusic
and VolumeSFX
.
An AudioMixer
is an asset that resides in the project folder and needs to be created and setup manually in the Editor.
It can be created by right-clicking in the Project Window
or under Assets
and then Create -> Audio Mixer
.
This will automatically create the Master
group.
To access the volume of a Mixer Group, an Exposed Parameter
has to be created.
Selecting the Master
group and right-clicking on the volume property in the inspector allows exposing the parameter.
Double-clicking the Audio Mixer
asset or navigating to Window -> Audio -> Audio Mixer
will open the Audio Mixer Window
.
Once opened, the name of the parameter can be changed under the Exposed Parameters
dropdown by double-clicking it.
This is an important step! The name given here, is how the group will be globally accessible by the SoundManager
.
Any other custom groups must be added under the Groups
section by clicking the +
button.
Just like before, exposing the volume parameters manually is unfortunately a mandatory step!
To let the SoundManager
know, which AudioMixer
volume groups it should manage, they have to be registered and unregistered.
This can be done via scripting or the Editor.
It is straightforward by code, however the methods expect an instance of type MixerVolumeGroup
.
This is a custom class that provides various functionality for handling a volume group in an AudioMixer
.
// Injects AudioMixer asset via Editor Inspector
[SerializeField] private AudioMixer audioMixer;
// Creates a MixerVolumeGroup instance with 'audioMixer' & the pre-setup exposed parameter 'VolumeMusic'
MixerVolumeGroup mixerVolumeGroup = new(audioMixer, "VolumeMusic", volumeSegments: 10);
// *** Volume segments can optionally be defined for allowing incremental/decremental volume change.
// This can e.g. be useful in segmented UI controls. ***
// Registers & Unregisters 'mixerVolumeGroup' with & from the SoundManager
SoundManager.Instance.RegisterMixerVolumeGroup(mixerVolumeGroup);
SoundManager.Instance.UnregisterMixerVolumeGroup(mixerVolumeGroup);
// *** It is important, that the exposed parameter exists in the referenced AudioMixer.
// Otherwise an error will be thrown! ***
Registering via Editor can be done through the Sound Volume Mixer component or the SoundManager
in the scene.
For the latter, right-clicking in the Hierarchy
or under GameObject
and then Audio -> Sound Manager
will create an instance.
Any groups can then be added in the list of Mixer Volume Groups Default
.
If left empty, the SoundManager
will register and unregister the groups contained in the AudioMixerDefault
asset automatically!
Setting a volume can only be done in percentage values (range 0 - 1).
Increasing and decreasing in steps requires the volume segments of the group to be more than 1.
// Sets volume of 'VolumeMusic' group to 0.5
SoundManager.Instance.SetMixerGroupVolume("VolumeMusic", .5f);
// Incrementally sets volume of 'VolumeMusic' group
// With volumeSegments = 10, this will result in a volume of 0.6
SoundManager.Instance.IncreaseMixerGroupVolume("VolumeMusic");
// Decrementally sets volume of 'VolumeMusic' group
// With volumeSegments = 10, this will result in a volume of 0.5 again
SoundManager.Instance.DecreaseMixerGroupVolume("VolumeMusic");
Muting and unmuting sets the volume to a value of 0 or restores to the previously stored unmuted value.
// Sets volume of 'VolumeMusic' group to 0.8
SoundManager.Instance.SetMixerGroupVolume("VolumeMusic", .8f);
// Mutes volume of 'VolumeMusic' group
SoundManager.Instance.MuteMixerGroupVolume("VolumeMusic", true);
// Equivalent to above
SoundManager.Instance.SetMixerGroupVolume("VolumeMusic", 0f);
// Unmutes volume of 'VolumeMusic' group back to value 0.8
SoundManager.Instance.MuteMixerGroupVolume("VolumeMusic", false);
Fading requires to set a targetVolume
and duration
.
// Fades volume of 'VolumeMusic' group to 0 over 2 seconds
SoundManager.Instance.FadeMixerGroupVolume("VolumeMusic", 0f, 2f);
// Same as above but with InOutCubic easing & onComplete callback
SoundManager.Instance.FadeMixerGroupVolume(
"VolumeMusic", 0f, 2f, ease: InOutCubic, onComplete: () => Debug.Log("Volume was smoothly muted!"));
// Similar to above as async call
await SoundManager.Instance.FadeMixerGroupVolumeAsync(
"VolumeMusic", 0f, 2f, ease: InOutCubic, cancellationToken: someCancellationToken);
Debug.Log("Volume was smoothly muted!")
Simplified linear cross-fading is also supported.
It will fade out the first group to a volume of 0 and fade in the other to 1.
// Fades volume of 'VolumeMusic' out & fades in 'VolumeDialog' over 1 second
SoundManager.Instance.CrossFadeMixerGroupVolumes("VolumeMusic", "VolumeDialog", 1f);
// Same as above as async call
await SoundManager.Instance.CrossFadeMixerGroupVolumesAsync("VolumeMusic", "VolumeDialog", 1f);
For any finer controlled cross-fading, it is recommended to call multiple fades simultaneously.
The Sound Emitter
component is a simple way of adding a sound, that is handled by the Sound Manager
.
It can be attached to any gameObject or created by right-clicking in the Hierarchy
or under GameObject
and then Audio -> Sound Emitter
.
An AudioSource
component will automatically be added if not already present.
Through it and the Configurations
Play
, Stop
and Fade
it is possible to define the sound behaviour in detail.
The component grants access to the following public methods:
- Play: Starts sound playback, if not already playing.
- Pause: Interrupts sound playback, if not currently stopping.
- Resume: Continues sound playback, if paused and not currently stopping.
- Fade: Fades volume of currently playing sound to the given target volume.
- Stop: Stops sound playback before completion (the only way, when looped).
Calling the methods will always apply the respective configurations. It is therefore important to set them before entering Play Mode
!
The image below shows an example usage of a Button
component and how one could invoke the methods via the onClick
event.
A Sound Volume Mixer
simplifies volume mixing of an Audio Mixer
group and uses the Sound Manager
for it.
This component can be added to any GameObject in the scene.
Or by creating it via right-clicking in the Hierarchy
or using the GameObject
menu, then choosing Audio -> Sound Volume Mixer
.
For the component to work, a reference to the Audio Mixer
asset is mandatory and the Exposed Parameter
name of the group has to be defined.
Section Mandatory Setup explains how to create such group and expose a parameter for it.
The following methods of the component are usable:
- Set: Changes group volume to the given value instantly.
- Increase: Instantly raises group volume step-wise (e.g. for
Volume Segments = 10
: +10%). - Decrease: Instantly lowers group volume step-wise (e.g. for
Volume Segments = 10
: -10%). - Fade: Fades group volume to the given target volume.
- Mute: Either mutes or un-mutes group volume.
Fade Configuration
determines, how volume fading will behave and must be setup before entering Play Mode
!
An example usage of the above methods can be seen below with a Button
component and its onClick
event.
This package is under the MIT License.