유니티에서 사운드를 관리하는 방법을 나름대로 써봤다.

이전에 VR 프로젝트를 진행하면서 오디오 매니저 및 오디오 시스템을 구축했었다. 이번 포스트에서 당시 만들었던 오디오 매니저와 오디오 관리 기법등을 적어본다. 추가적으로 네트워크 상에서의 재생을 위해 Photon PUN2를 활용해서 재생하는 방법까지 알아보자

사용하고자 하는 오디오 클립들을 관리하기 위한 스크립터블 오브젝트다. 클립에게 속성을 부여하고, 밑에 기술하는 오디오 데이터베이스에서 사용하는 데이터다. 아래와 같은 속성을 가진다.
[System.Serializable]
[CreateAssetMenu(fileName = "NewSoundData", menuName = "Audio/Data") ]
public class AudioData : ScriptableObject
{
public string clipName;
public AudioClip clipSource;
[Range(0f,1f)] public float volume = 1.0f;
public bool loop = false;
public AudioMixerGroup mixerGroup;
}

오디오 데이터들을 담는 데이터베이스. 리스트 형태로 가지고 있고, 새로운 오디오 데이터가 추가 될 때마다 직접 드래그&드롭으로 추가해준다.
[CreateAssetMenu(fileName = "NewAudioDataBase",menuName = "Audio/DataBase")]
public class AudioDataBase : ScriptableObject
{
public List<AudioData> audioList;
}
스크립트가 RPC 함수를 수행하려면 2가지 조건이 필요하다.
MonoBehaviourPun 또는 MonoBehaviourPunCallbacks를 상속받는다.
게임 오브젝트가 PhotonView를 컴포넌트로 가진다.
해당 조건을 만족하기 위해 MonoBehaviourPun을 상속하는 SingletonPun을 만들었다.
using Photon.Pun;
using UnityEngine;
public class SingletonPun<T> : MonoBehaviourPun where T : MonoBehaviourPun
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<T>();
if (instance != null) return instance;
GameObject go = new GameObject($"{typeof(T)}");
instance = go.AddComponent<T>();
DontDestroyOnLoad(go);
}
return instance;
}
}
protected virtual void Awake()
{
Init();
}
protected virtual void Init()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
Destroy(gameObject);
}
}
protected void OnApplicationQuit()
{
instance = null;
}
public virtual void DestroyManager()
{
if (instance != null)
{
Destroy(gameObject);
}
}
}
오디오 데이터베이스를 가져와서 딕셔너리<이름string, 오디오데이터>를 만들어서 사용한다. 오디오를 출력할 믹서 그룹을 참조한다. 재생할 때, 오디오 소스의 재생 믹서 그룹을 정해준다.
오디오 매니저는 다음과 같은 기능을 가진다.
오디오 출력을 그룹으로 묶어서 관리할 수 있게 해주는 기능이다. 여러가지 기능을 가지고 있다. 스크립트 상으로 관리가 가능하다.

프로젝트 창에서 위와 같이 추가가 가능하다.


현재 믹서의 설정 상황을 스냅샷 형태로 저장한다. 스냅샷을 불러오는 것으로 저장해둔 값을 불러올 수 있다.

우리가 믹서를 쓰는 이유다. 오디오를 원하는 대로 그룹으로 묶어서 출력이 가능하게 해준다. 각 그룹마다 서로 다른 효과를 적용 시킬 수 있고, 볼륨 컨트롤이 가능하다.

생성된 믹서를 더블클릭하고, 사진의 + 버튼을 누르면 그룹이 추가된다. 그룹 안의 그룹이 가능하기 때문에 주의하자.
오디오 믹서의 볼륨은 데시벨(decibel, db)로 관리된다. 데시벨의 범위는 -80 ~ 0 .
데시벨은 상용로그( )의 로그 스케일로 표현된다. 그래서 데시벨이 10 증가할 때마다 기존 값에서 10배 증가한다. 이러한 특징으로 인해 UI의 값과 연동 하려면 Mathf의 log를 써야 한다.
audioMixer.SetFloat("MasterVolume", Mathf.Log10(Mathf.Clamp(value, 0.0001f, 1f)) * 20f);
여기서, 클램프의 최소 값이 0.0001f인 이유는, 로그에서 0은 정의되지 않기 때문이다. 0.0001f 값은 데시벨로 -80db인데, 거의 무음이라고 할 수 있다.
만들어 둔 오디오 믹서 그룹을 오디오 매니저와 연결해보자. 스크립트 상으로 불러오려면, AudioMixer, AudioMixerGroup으로 참조해야 한다. 직접 드래그&드롭으로 참조 시킬 수 있지만, 우리는 Resources.Load기능으로 불러와 본다.
// 믹서 세팅
private AudioMixer mixer; // 볼륨을 관리하는 오디오 믹서
private AudioMixerGroup masterGroup; // 마스터그룹. bgmGroup과 sfxGroup의 출력을 최종적으로 결정한다.
private AudioMixerGroup bgmGroup;
private AudioMixerGroup sfxGroup;
// 믹서 및 그룹 연결
mixer = Resources.Load<AudioMixer>("Audio/AudioMixer");
bgmGroup = mixer.FindMatchingGroups("Master/BGM")[0]; // 경로에 상위 Mixer를 써주는 것이 주의점이다.
sfxGroup = mixer.FindMatchingGroups("Master/SFX")[0];

스크립트 상에서 믹서의 볼륨에 접근하려면, Exposed Parameter에 추가해줘야 한다.

여기서 Expose를 해주면 이제 스크립트에서 접근이 가능하다.

접근가능한 string 이름은 여기서 볼 수 있다.


이름을 직접 변경해줄 수 있다. 알아보기 쉽게 Volume 글자만 추가해줬다.
[System.Serializable]
[CreateAssetMenu(fileName = "NewSoundData", menuName = "Audio/Data") ]
public class AudioData : ScriptableObject
{
public string clipName;
public AudioClip clipSource;
[Range(0f,1f)] public float volume = 1.0f;
public bool loop = false;
public AudioMixerGroup mixerGroup; // 새로이 믹서 그룹을 추가해준다.
}

오디오 데이터 생성 단계에서 미리 믹서 그룹을 정해두고, 이를 통해 오디오 매니저에서 자동으로 라우팅하게 한다.
if(outClip.mixerGroup)
{
outSource.outputAudioGroup = outClip.mixerGroup; // 출력하는 오디오 소스의 믹서 그룹을 오디오데이터가 정한다.
}
이제 슬라이더 UI의 값을 믹서그룹의 볼륨 값에 적용 시켜보자. 앞서 설명했듯이 볼륨은 데시벨로 로그 스케일이기 때문에 0에서 1까지 들어오는 슬라이더의 값을 변환해야한다.
public enum MixerType { Master, BGM, SFX }
public void SetAudioVolume(MixerType type, float volume) // 마스터 볼륨 설정. 0~1 사이의 값이 들어온다.
{
switch (type)
{
case MixerType.Master:
mixer.SetFloat("MasterVolume", Mathf.Log10(Mathf.Clamp(volume,0.0001f,1f))*20);
PlayerPrefs.SetFloat("MasterVolume", volume); //PlayerPrefs로 설정 값을 저장한다.
break;
case MixerType.BGM:
mixer.SetFloat("BGMVolume", Mathf.Log10(Mathf.Clamp(volume,0.0001f,1f))*20);
PlayerPrefs.SetFloat("BGMVolume", volume);
break;
case MixerType.SFX:
mixer.SetFloat("SFXVolume", Mathf.Log10(Mathf.Clamp(volume,0.0001f,1f))*20);
PlayerPrefs.SetFloat("SFXVolume", volume);
break;
default:
Debug.LogWarning($"믹서 타입이 잘못들어옴{type}");
return;
}
}
간단하게 설정 값을 로컬에 저장하는 용도로 쓸 수 있는 json 저장 방식이다. 딕셔너리로 관리되며, 로컬 클라이언트에 값을 저장하는 것이기 때문에 게임을 다시 켜도 저장한 값이 유지된다. 게임의 세이브 용도로는 쓰기에 부적합하다.
private void VolumeLoad() // 사용자 설정을 불러오기
{
float value;
if (PlayerPrefs.HasKey("MasterVolume")) // 로컬에 해당 키에 대한 값이 있는지 먼저 확인한다.
{
value = PlayerPrefs.GetFloat("MasterVolume");
mixer.SetFloat("MasterVolume", Mathf.Log10(Mathf.Clamp(value,0.0001f,1f))*20);
// 0 ~ 1로 저장된 값을 다시 데시벨의 로그 스케일로 바꿔주는 수식이다.
}
if (PlayerPrefs.HasKey("BGMVolume"))
{
value = PlayerPrefs.GetFloat("BGMVolume");
mixer.SetFloat("BGMVolume", Mathf.Log10(Mathf.Clamp(value,0.0001f,1f))*20);
}
if (PlayerPrefs.HasKey("SFXVolume"))
{
value = PlayerPrefs.GetFloat("SFXVolume");
mixer.SetFloat("SFXVolume", Mathf.Log10(Mathf.Clamp(value,0.0001f,1f))*20);
}
}

음원 파일이 무손실로 너무 큰 파일을 들여올 경우, 오디오 소스에서 클립을 전환 할 때 끊김 현상이 발생한다.
음원 파일이 크면 클 수록 메모리에 바로 올리는 데에 시간이 더 오래걸리기 때문이다. 유니티가 음원파일을 임포트할 때 자동으로 Vorbis 포멧으로 압축하여 가져오지만, 위와 같이 20메가가 넘는 파일이다보니 1초정도의 딜레이가 생기게 된다.
