Tutorial – Simple Sound Manager for Unity3D

UPDATE: ALL THESE AND MORE CAN BE FOUND ON MY NEW SITE http://www.returnofthebrain.co.uk/

Note: This tutorial was made in Unity 5.1 but should work for older versions. If you have any questions I can be reached easily on twitter @The_A_Drain

The goal today is to create a nice simple sound utility that we can use to control background music and sound effects, as well as giving us a global volume and the ability to mute/unmute the sound in our game.

What we’re going to produce isn’t massively complex and doesn’t take 3D audio into account, but it is incredibly useful for game jams and smaller games that don’t have complex audio requirements. I use this exact script for pretty much all my game jam and prototype games as it allows me to fire-and-forget sound effects and fade background music with a single function call. It’s a great thing to have in your development toolbox.

Disclaimer: This does mean that there isn’t going to be many visuals this time! This is a very text-heavy tutorial.

First thing we’ll do is set up the class itself as a singleton that we can access an instance of from anywhere. We store a local static instance of our class and then other classes and static methods can use GetInstance to access it. If there isn’t an instantiated instance, we create a new game object and attach our script to it before returning it. This way we don’t need to do any manual setup for this script within the editor. Everything is handled in script.

using UnityEngine;
using System.Collections.Generic;
using System.Collections;

public class SoundManager : MonoBehaviour
{

    // Static instance
    static SoundManager _instance;
    public static SoundManager GetInstance()
    {
        if (!_instance)
        {
    	    GameObject soundManager = new GameObject("SoundManager");
	    _instance = soundManager.AddComponent();
	    _instance.Initialize();
        }

        return instance;
    }

Next we need to create a few variables to track volume. We’ve got a maximum volume for background music and sound effects, while not strictly necessary it’s nice to be able to adjust this if we need to.

Then we have a couple of static variables to represent the current volume as a normalized value between 0 and 1 which we’ll multiply the maximum volume by to determine actual volume. And a variable to track whether or not the sound is muted.

Lastly we have two instance variables, a list of AudioSource components that will play sound effects and a single AudioSource to play our background music. We’ll need multiple sound effects sources to be able to play overlapping sound effects and avoid having new sounds cancel sounds that are already playing.

Our Initialize method sets up the background music source by adding the component, setting it to loop, disabling playOnAwake and setting the initial volume.

Lastly we call DontDestroyOnLoad so that our instance will persist through scene changes.

    const float MaxVolume_BGM = 1f;
    const float MaxVolume_SFX = 1f;
    static float CurrentVolumeNormalized_BGM = 1f;
    static float CurrentVolumeNormalized_SFX = 1f;
    static bool isMuted = false;

    List sfxSources;
    AudioSource bgmSource;
    void Initialize()
    {
        // add our bgm sound source
        bgmSource = gameObject.AddComponent();
        bgmSource.loop = true;
        bgmSource.playOnAwake = false;
	bgmSource.volume = GetBGMVolume();
        DontDestroyOnLoad(gameObject);
    }

Next up we’re going to write a couple of helper methods for getting the current adjusted volume. The logic here is very simple, we either return zero if sound is muted, or we return the maximum volume multiplied by the current normalized volume.

     // ==================== Volume Getters =====================

    static float GetBGMVolume () {
	return isMuted ? 0f : MaxVolume_BGM * CurrentVolumeNormalized_BGM;
    }

    public static float GetSFXVolume () {
 	return isMuted ? 0f : MaxVolume_SFX * CurrentVolumeNormalized_SFX;
    }

First thing we’re going to deal with is background music, and there’s a couple of major things we need to be able to do for our simple sound system.

  • Play a track with or without a fade up
  • Fade out and into another track
  • Stop playing the track

Starting and stopping audio playback is easy using the AudioSource Monobehaviors built-in functions, but fading requires a little bit more work.

To save us duplicating code we’re going to use a single fade coroutine, FadeBGM, that will facilitate fading to a specific volume rather than just up or down, and then call that function from elsewhere.

We pass in a target volume, a delay if we need one, and a duration. Then we just lerp the background audio source’s volume to where it needs to be before exiting.

FadeBGMIn takes in an the audio track as an audioclip, and the delay/duration parameters it passes on to FadeBGM. It then sets the track as our background audio sources clip and tells it to play before starting the fade coroutine.

FadeBGM Out just takes in a duration and starts fading with a target volume of zero.

    // ====================== BGM Utils ======================

    void FadeBGMOut (float fadeDuration)
    {
        SoundManager soundMan = GetInstance();
        float delay = 0f;
        float toVolume = 0f;

        if(soundMan.bgmSource.clip == null){
            Debug.LogError("Error: Could not fade BGM out as BGM AudioSource has no currently playing clip.");
        }

        StartCoroutine(FadeBGM(toVolume, delay, fadeDuration));
     }

     void FadeBGMIn (AudioClip bgmClip, float delay, float fadeDuration)
     {
         SoundManager soundMan = GetInstance();
         soundMan.bgmSource.clip = bgmClip;
         soundMan.bgmSource.Play();

         float toVolume = GetBGMVolume();

         StartCoroutine(FadeBGM(toVolume, delay, fadeDuration));
     }

     IEnumerator FadeBGM(float fadeToVolume, float delay, float duration)
     {
         yield return new WaitForSeconds (delay);

         SoundManager soundMan = GetInstance();
         float elapsed = 0f;
         while (duration > 0) {
             float t = (elapsed / duration);
             float volume = Mathf.Lerp (0f, fadeToVolume*CurrentVolumeModifier_BGM, t);
 soundMan.bgmSource.volume = volume;

             elapsed += Time.deltaTime;
             yield return 0;
         }
     }

Next up are two static functions that will actually handle playing and stopping background music on our instance. These are designed to be called from anywhere without even needing to manually call GetInstance().

PlayBGM takes in our track, a bool that determines whether or not we should fade and a duration for that fade. It fetches the sound manager instance before doing anything.

If we have told PlayBGM we want to fade, it will determine whether we need to first fade-out a currently playing track first, in which case it will fade down the current track and fade up the new one after a delay. Otherwise it will simple fade up the new track. Lastly if we’ve told it we don’t want to fade, it just grabs the audio source and starts playing the track immediately.

StopBGM is simpler, it just gets the soundmanager instance and tells the background source to stop playing immediately if we don’t want to fade, or to FadeBGMOut with the supplied duration if we do.

    // ====================== BGM Functions ======================

    public static void PlayBGM(AudioClip bgmClip, bool fade, float fadeDuration)
    {
 	SoundManager soundMan = GetInstance();

	if (fade) {
	    if (soundMan.bgmSource.isPlaying) {
		// fade out, then switch and fade in
		soundMan.FadeBGMOut(fadeDuration/2);
		soundMan.FadeBGMIn (bgmClip, fadeDuration / 2, fadeDuration / 2);

	    } else {
		// just fade in
		float delay = 0f;
		soundMan.FadeBGMIn(bgmClip, delay, fadeDuration);
	    }
	} else {
	    // play immediately
	    soundMan.bgmSource.volume = GetBGMVolume ();
	    soundMan.bgmSource.clip = bgmClip;
	    soundMan.bgmSource.Play ();
	}
    }

    public static void StopBGM(bool fade, float fadeDuration){
	SoundManager soundMan = GetInstance();
	if (soundMan.bgmSource.isPlaying) {
		// fade out, then switch and fade in
                if(fade){
                     soundMan.FadeBGMOut(fadeDuration);
                } else {
		     soundMan.bgmSource.Stop();
                }
	}
    }

Now we need to write some similar functions for our sound effects. Juggling lots of potential audio sources is a little different from just the one, however. So we need a couple of functions to help with removal of expired sources and a function to handle getting a new one.

GetSFXSource handles creating a new audio source to us to use and returning it. We simply create a new audio source by adding it to our gameObject, set loop and playOnAwake to false and set the volume. We then instantiate our sfx source list if it’s not already been done before adding it to the list and returning the audio source object for use.

RemoveSFXSource is a coroutine that takes in an audiosource and waits for the duration of the clip before removing it from the audio source list and destroying the audio source. This is just a helper coroutine that we can call when we play the sound effect so that it will be cleaned up once it’s done.

RemoveSFXSourceFixedLength does the same thing but with a duration we provide rather than the duration of the clip. This can be useful if we know we want to stop a sound effect early.

    // ======================= SFX Utils ====================================

    AudioSource GetSFXSource()
    {
        // set up a new sfx sound source for each new sfx clip
	AudioSource sfxSource = gameObject.AddComponent();
	sfxSource.loop = false;
	sfxSource.playOnAwake = false;
	sfxSource.volume = GetSFXVolume();

	if (sfxSources == null)
	{
	    sfxSources = new List();
	}

	sfxSources.Add(sfxSource);

	return sfxSource;
    }

    IEnumerator RemoveSFXSource(AudioSource sfxSource)
    {
	yield return new WaitForSeconds(sfxSource.clip.length);
	sfxSources.Remove(sfxSource);
	Destroy(sfxSource);
    }

    IEnumerator RemoveSFXSourceFixedLength(AudioSource sfxSource, float length)
    {
 	yield return new WaitForSeconds(length);
	sfxSources.Remove(sfxSource);
	Destroy(sfxSource);
    }

Next up are the functions responsible for actually playing our sound effects. We want to be able to do a few things here.

  • Play a sound effect
  • Play a sound effect with a slightly randomized pitch
  • Play a sound effect with a fixed duration

These are all relatively simple but provide a good deal of functionality.

PlaySFX is the simplest, all we do here is get the SoundManager instance, fetch a new SFXSource and use it to play the clip before stating the RemoveSFXSource coroutine to clean it up.

PlaySFXRandomized does exactly the same thing but adjusts the pitch slightly using a random value. This is useful for effects that will be played often and just need a very slight variation so as not to become too repetitive. Good for bullets, explosions, footsteps, etc.

Lastly PlaySFXFixedDuration does the same as PlaySFX but cuts off the clip at a specified point.

    // ====================== SFX Functions =================================

    public static void PlaySFX(AudioClip sfxClip)
    {
        SoundManager soundMan = GetInstance();
        AudioSource source = soundMan.GetSFXSource();
		source.volume = GetSFXVolume ();
        source.clip = sfxClip;
        source.Play();

        soundMan.StartCoroutine(soundMan.RemoveSFXSource(source));
    }

    public static void PlaySFXRandomized(AudioClip sfxClip)
    {
        SoundManager soundMan = GetInstance();
        AudioSource source = soundMan.GetSFXSource();
		source.volume = GetSFXVolume ();
        source.clip = sfxClip;
        source.pitch = Random.Range(0.85f, 1.2f);
        source.Play();

        soundMan.StartCoroutine(soundMan.RemoveSFXSource(source));
    }

    public static void PlaySFXFixedDuration(AudioClip sfxClip, float duration, float volumeMultiplier = 1.0f)
    {
        SoundManager soundMan = GetInstance();
        AudioSource source = soundMan.GetSFXSource();
        source.volume = GetSFXVolume() * volumeMultiplier;
        source.clip = sfxClip;
        source.loop = true;
        source.Play();

        soundMan.StartCoroutine(soundMan.RemoveSFXSourceFixedLength(source, duration));
    }

Last thing we need is a set of functions to control enabling/disabling sound and adjusting volume.

DisableSoundImmediate will stop all currently playing sound effects, set the background source volume to zero and set the isMuted attribute so that future requests for volume will return zero.

EnableSoundImmediate re-enabled all of our audio sources and our background source and sets isMuted to false so that GetVolume will return the correct volume levels from now on.

Set Global, BGM and SFX volume just sets the current volume either for BGM, SFX or both and calls AdjustSoundImmediate to update volume levels.

AdjustSoundImmediate will actually update all of our sources to reflect the new volume level by looping through our list of sfx sources and updating our bgm source volume.

    // ==================== Volume Control Functions ==========================

    public static void DisableSoundImmediate()
    {
        SoundManager soundMan = GetInstance();
        soundMan.StopAllCoroutines();
        if (soundMan.sfxSources != null)
        {
            foreach (AudioSource source in soundMan.sfxSources)
            {
                source.volume = 0;
            }
        }
        soundMan.bgmSource.volume = 0f;
        isMuted = true;
    }

    public static void EnableSoundImmediate()
    {
        SoundManager soundMan = GetInstance();
        if (soundMan.sfxSources != null)
        {
            foreach (AudioSource source in soundMan.sfxSources)
            {
                source.volume = GetSFXVolume();
            }
        }
        soundMan.bgmSource.volume = GetBGMVolume();
	isMuted = false;
    }

    public static void SetGlobalVolume(float newVolume)
    {
        CurrentVolumeNormalized_BGM = newVolume;
        CurrentVolumeNormalized_SFX = newVolume;
        AdjustSoundImmediate();
    }

    public static void SetSFXVolume(float newVolume){
        CurrentVolumeNormalized_SFX = newVolume;
        AdjustSoundImmediate();
    }

    public static void SetBGMVolume(float newVolume){
        CurrentVolumeNormalized_BGM = newVolume;
        AdjustSoundImmediate();
    }

    public static void AdjustSoundImmediate()
    {
        SoundManager soundMan = GetInstance();
        if (soundMan.sfxSources != null)
        {
            foreach (AudioSource source in soundMan.sfxSources)
            {
                source.volume = GetSFXVolume();
            }
        }
        Debug.Log("BGM Volume: "+ GetBGMVolume());
        soundMan.bgmSource.volume = GetBGMVolume();
        Debug.Log("BGM Volume is now: " + GetBGMVolume());
    }
}

And we’re done! This script can just sit happily inside a utilities folder in your project and you can call it from any other script with just a single line of code and pass in your AudioClip object.

To play a sound effect:

    SoundManager.PlaySFX(audioClip);

To play background music or fade into a new track:

    SoundManager.PlayBGM(audioClip, shouldFade, fadeDuration);

It’s really that simple.

There’s plenty of room for expanding this script, too. You could add a second BGM audio source and update the fade functions to allow cross-fading so that one track can fade into another rather than having to stop first, for example. Quite easily, too. Give it a try!

Advertisements
This entry was posted in Game Design, Tutorials, Unity3D and tagged , , , . Bookmark the permalink.

9 Responses to Tutorial – Simple Sound Manager for Unity3D

  1. Alex says:

    List sfxSources;
    list of what excatly?

    Like

    • diablobasher says:

      Hi!

      My bad! It’s a list of AudioSources, the editor here sometimes strips out things between tags every time I save it :\

      Like

      • Alex says:

        thx! I have several questions but 1st of all great manager
        But can you rewrite this
        // add our bgm sound source
        bgmSource = gameObject.AddComponent();
        bgmSource.loop = true;
        bgmSource.playOnAwake = false;
        bgmSource.volume = GetBGMVolume();
        DontDestroyOnLoad(gameObject);

        I’m saying that because it is kinda heavy, it will take more time and performance ^^”

        other thing can u please explain the “if statement” for both BK and SFX
        return isMuted ? 0f : MaxVolume_BGM * CurrentVolumeNormalized_BGM;
        or atleast can u write it for me in a normal if else statement cuz im not familiar with that one, i mean i know it’s a if else in a way but i don’t really use it much so i dont understand it xD! ty u again for ur reply!!

        Like

  2. Alex says:

    Also, one more question and sorry for spamming you but is there a function to toggle audio on and off for SFX only?

    Like

    • diablobasher says:

      Hi, I’ll try and respond to all your questions:

      I’m not sure why you think that first portion of script is performance heavy, but you can make whatever changes to it you feel you need to.

      “return isMuted ? 0f : MaxVolume_BGM * CurrentVolumeNormalized_BGM;”

      What this piece of code does is returns 0 if the volume is muted, and if it’s not muted it returns the current volume based on the normalized (between 0 and 1) volume value and the maximum volume.

      If you want to know more about the ternary operator you can read about it here: https://msdn.microsoft.com/en-gb/library/ty67wk28.aspx

      You can easily make a function to toggle SFX and BGM seperately, just move the SFX code into a separate function and rename the existing one to ToggleBGM.

      Like

  3. The coroutine FadeBGM function checks for duration > 0 but the value is actually never decreased so it will run indefinitely.

    while (duration > 0) {
    float t = (elapsed / duration);
    float volume = Mathf.Lerp (0f, fadeToVolume*CurrentVolumeModifier_BGM, t);
    soundMan.bgmSource.volume = volume;

    elapsed += Time.deltaTime;
    yield return 0;
    }

    Liked by 1 person

  4. Laura says:

    Super easy to follow, helped me a lot!

    Like

  5. QiFo Lin says:

    There r some type errors ,i paste right one:
    public static SoundManager GetInstance()
    {
    if (!_instance)
    {
    GameObject soundManager = new GameObject(“SoundManager”);
    _instance = soundManager.AddComponent();
    _instance.Initialize();
    }

    return _instance;
    }

    And we need stop sfx
    ///
    /// 播放音效
    ///
    ///
    /// 是否循环
    ///
    public static AudioSource PlaySFX(AudioClip sfxClip,bool isLoop = false)
    {
    SoundManager soundMan = GetInstance();
    AudioSource source = soundMan.GetSFXSource();
    source.volume = GetSFXVolume();
    source.clip = sfxClip;
    source.loop = isLoop;
    source.Play();
    if (!isLoop)
    {
    soundMan.StartCoroutine(soundMan.RemoveSFXSourceWhenFinish(source));
    }
    return source;
    }
    ///
    /// 停止音效
    ///
    public static void StopSFX(AudioSource source)
    {
    SoundManager soundMan = GetInstance();
    soundMan.RemoveSFXSourceImmediate(source);
    }

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s