Thursday, March 28, 2013

Unity C# AudioManager Tutorial - Part 4 (Music, Pausing, & Voice Over)

This is part 4 of a 4 part tutorial to create an audio manager for unity. This tutorial focuses on playing music, pausing fx while keeping music playing, and controlling sound volume including fading.

Part 1 | Part 2 | Part 3 | Part 4

Playing Music

We'll need a couple more key concepts to play music, but the first is easy. We want music to loop, so we can just call the PlayLoop method with our music clip. In order to distinguish the music from other sounds playing, we will create a member variable m_activeMusic in the AudioManager that will hold a reference to this particular AudioSource. Unless otherwise noted, all code in this tutorial is in our AudioManager class.
    private AudioSource m_activeMusic;
    public AudioSource PlayMusic(AudioClip music, float volume) {
        m_activeMusic = PlayLoop(music, transform, volume);
        return m_activeMusic;
    }
Notice that no transform parameter is passed in to identify the music position. Here I am assuming that in game music always plays at its given volume, and is not affected by any object movement in the game. To achieve this, we can to attach the music sound to the transform of the AudioManager, which, as explained in part 1, is attached to the camera, giving it a constant distance from the audio listener of 0.

The PlayLoop method adds the music sound to our m_activeAudio, but with m_activeMusic we can track this particular sound special, which we will utilize in the next section, fx pause control.

Pause Control

Now that we have a list of active sounds in m_activeAudio, we can pause them on command. With our reference to the music, we can indicate that the pause function only pauses all active sounds that are NOT the music:
public void pauseFX() { 
    foreach (var audioClip in m_activeAudio) {
        try {
            if (audioClip.source != m_activeMusic) {
                audioClip.source.Pause();
            }
        } catch {
            continue;
        }
    }
}

public void unpauseFX() {
    foreach (var audioClip in m_activeAudio) {
        try {
            if (!audioClip.source.isPlaying) {
                audioClip.source.Play();
            }
        } catch {
            continue;
        }
    }
}
Fairly straight forward- when AudioManager.Instance.pauseFX() is called, the method cycles through each of the active sounds, and if the current iterator is not the music, it calls Unity's built in 'AudioSource.pause' function. The try/catch block is to catch any timing issues where the audio source is destroyed while this is iterating through the list. If there are any issues on a given element in the list, the method catches the error and simply skips that element, and next time we pause or unpause, the ClipInfo object that errored out on this pass should have been removed in our updateActiveAudio method from part 3.

When the unpauseFX function is called, it goes through the active sounds, and if any are not playing, it plays them. If the audio is not paused and not playing, Unity automatically resumes them from where they paused. If you have any issues where sounds are restarting unexpectedly, this is the place to check first.

Voice Overs

To make sure we can clearly hear a voiceover, we're going to make all sounds that are currently playing quieter while the voiceover is playing. Similar to music, we create a member variable that tracks the active voiceover that is playing. Also like music, we assume that the voiceover is full volume regardless of object positions in the world, so we attach the voice over to the AudioManager transform (the camera.) We're also going to need a volumeMod variable that holds the amount to reduce all the other sounds' volume, and set it low when the voice over is started.
private AudioSource m_activeVoiceOver;
private float m_volumeMod = 1.0f;
public AudioSource PlayVoiceOver(AudioClip voiceOver, float volume) {
    AudioSource source = Play(voiceOver, transform, volume);
    m_activeVoiceOver = source;
    m_volumeMod = 0.2f;
    return source;
}
Now that we have m_volumeMod, we can revisit our updateActiveAudio method to add volume modification to all sounds. Before we loop through the sounds, we will check if m_activeVoiceOver is set, and if it is not, we will set the volume mod to 1.0 (normal volume.) Otherwise, we will keep it's current value, which should be set at 0.2 (as in the PlayVoiceOver method above). Then, when iterating through the active audio, we will check if the audio is our m_activeVoiceOver, and if it is not, we will make the current volume its default volume multiplied by the volume modifier. This makes all sounds but the voice over affected by the volume mod:
private void updateActiveAudio() {
    var toRemove = new List<ClipInfo>();
    try {
        if (!m_activeVoiceOver) {
            m_volumeMod = 1.0f;
        }
        foreach (var audioClip in m_activeAudio) {
            if (!audioClip.source) {
                toRemove.Add(audioClip);
            } else if (audioClip.source != m_activeVoiceOver) {
                audioClip.source.volume = audioClip.defaultVolume * m_volumeMod;
            }
        }
    } catch {
       Debug.Log("Error updating active audio clips");
       return;
    }
    //cleanup
    foreach (var audioClip in toRemove) {
        m_activeAudio.Remove(audioClip);
    }
}
There you have it. You can extend this type of pausing/volume modification functionality to additional AudioSource variables if you have other types of sound events in your game. You can also use the concept behind this volumeMod variable to create an in-game master volume slider for the player.

Fading Sound

As a final bonus feature, we can make the voice over modification fade the other sounds in and out instead of just snapping to the volume modification. This is a more minor trick, and it requires a few new variables, but it has a pretty significant effect, though subtle in execution.

First we'll have to add two new variables. A min volume variable to establish how low the volume should go during voice overs, and a voice over fade bool to indicate if we are currently fading for the voice over or not. We'll initialize these new variables in our Awake method.
private float m_volumeMod, m_volumeMin;
private bool m_VOfade; //used to fade to quiet for VO

void Awake(){
    Debug.Log("AudioManager Initializing");
    try {
        transform.parent = GameObject.FindGameObjectWithTag("MainCamera").transform;
        transform.localPosition = new Vector3(0, 0, 0);
    } catch {
        Debug.Log("Unable to find main camera to put audiomanager");
    }
    m_activeAudio = new List<ClipInfo>();
    m_volumeMod = 1;
    m_volumeMin = 0.2f;
    m_VOfade = false;
    m_activeVoiceOver = null;
    m_activeMusic = null;
}
We will update our Update method to gradually decrease the volumeMod to our min volume if our VOfade bool is true, and gradually increase the volumeMod to 1 if our VOfade bool is false. We will have the PlayVoiceOver method activate our VOfade bool, and have the updateActiveAudio method check the m_activeVoiceOver AudioSource and deactivate VOfade if it is null (if the voiceover is not currently active):
void Update() {
    //fade volume for VO
    if (m_VOfade && m_volumeMod >= m_volumeMin) {
        m_volumeMod -= 0.1f;
    } else if (!m_VOfade && m_volumeMod < 1.0f) {
        m_volumeMod += 0.1f;
    }
    updateActiveAudio();
}

public AudioSource PlayVoiceOver(AudioClip voiceOver, float volume){
    AudioSource source = Play(voiceOver, transform, volume);
    m_activeVoiceOver = source;
    m_VOfade = true;
    return source;
}

private void updateActiveAudio() { 
    var toRemove = new List<ClipInfo>();
    try {
        if (!m_activeVoiceOver) {
            m_VOfade = false;
        }
        foreach (var audioClip in m_activeAudio) {
            if (!audioClip.source) {
                toRemove.Add(audioClip);
            } else if (audioClip.source != m_activeVoiceOver) {
                audioClip.source.volume = audioClip.defaultVolume * m_volumeMod;
            }
        }
    } catch {
        Debug.Log("Error updating active audio clips");
        return;
    }
    //cleanup
    foreach (var audioClip in toRemove) {
        m_activeAudio.Remove(audioClip);
    }
}
Note that I am adjusting m_volumeMod by 0.1 each update. This struck a nice balance for me between having it happen quickly enough to still be effective, but slow enough to keep a smoothness in the transition.

Conclusion

You've reached the end of the tutorial! You should now have a straightforward audio manager that acts as a singleton, allows clip playing and control from anywhere, tracks active sound clips, pauses select sounds, fades select sounds, and is simple to modify for further functionality. I hope this provided insight into a way to more easily manage audio in your projects. If you have any questions or feedback, please leave it in the comments, or feel free to contact me at baheard@gmail.com. Special thanks to Herman Tulleken and Daniel Rodriguez for their related posts.

5 comments:

  1. Any advice on where to put the audio clips before calling them with the method in this script?

    ReplyDelete
    Replies
    1. In the cases we're using, audio clips always exist on a gameobject as public member variables. Once you create the public member variables in the code, the audioclip member will appear in the unity inspector under that game object, and you can populate them with sound files from your hard drive there. So our 'TrainEngine' object will have "public AudioClip Horn' and 'public AudioClip Chugga", and then in the code you can pass these audioclips to the audiomanager.
      Alternatively, you could load the audio clips directly from the 'Resources' folder using AudioClip mySound = Resources.Load( [path] ). See http://docs.unity3d.com/ScriptReference/Resources.Load.html

      Delete
    2. Thanks very much for your reply Ben.

      So you just attach your sounds to objects in unity then you can call them with your methods in any other scripts?

      Do you still need a getcompnent reference in the other script too?

      Also which of the two options (objects or resources.load) would you recommend for mobile app in terms of efficiency etc..

      Delete
  2. That's right. In order to attach the sound to a prefab in unity, you need to make it a public member variable in the c# code. Once you have that, you can just reference the audio clip as the variable from inside the class. If you want to access the clip from another class, you'll need to call GetComponent to get the script with the audio clip variable, and then call the public member variable from that. Example:

    MyPrefabWithAudioClipScript.GetComponent().MyAudioClipMember

    I do not have metrics for differences in efficiency between resources.load vs a public variable in unity. I have heard that resources.load is slower. However, my recommendation for optimization in general is as follows:

    1. Code for readability first, optimize for performance later. It is easier to optimize clean code than to clean optimized code.
    2. If you're going to spend the time to optimize, spend the time to measure the difference in performance. Speculative optimization is uncertain, but certainly (usually) results in lower quality code (harder to read, harder to maintain, harder to update.)

    ReplyDelete
  3. What about assigning audio clips to audio mixer groups?

    ReplyDelete