Unity - Audio

땡구의 개발일지·2025년 8월 4일

Unity마스터

목록 보기
71/78

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

🎵 오디오 시스템

이전에 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;
}

🔧 PUN 싱글톤

스크립트가 RPC 함수를 수행하려면 2가지 조건이 필요하다.

  1. MonoBehaviourPun 또는 MonoBehaviourPunCallbacks를 상속받는다.

  2. 게임 오브젝트가 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, 오디오데이터>를 만들어서 사용한다. 오디오를 출력할 믹서 그룹을 참조한다. 재생할 때, 오디오 소스의 재생 믹서 그룹을 정해준다.
오디오 매니저는 다음과 같은 기능을 가진다.

  • SwitchBGM : 흘러나오는 음악을 정해 재생한다. 페이드 아웃/인이 적용된다.
  • Ambient : 백색소음을 재생한다. 마찬가지로 페이드 아웃/인이 적용된다.
  • 클립 : 3D 효과를 가지고 오디오 클립을 일시적으로 재생한다. 매개변수로 재생할 위치 값인 Vector3를 받는다.
  • 이펙트 : UI와 같은, 전역재생에 해당하는 클립 재생에 쓰인다.

🚩 오디오 믹서

오디오 출력을 그룹으로 묶어서 관리할 수 있게 해주는 기능이다. 여러가지 기능을 가지고 있다. 스크립트 상으로 관리가 가능하다.

🔧 추가


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

🎨 스냅샷


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

👨‍👩‍👧‍👦 오디오 믹서 그룹


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

📦 그룹 추가


생성된 믹서를 더블클릭하고, 사진의 + 버튼을 누르면 그룹이 추가된다. 그룹 안의 그룹이 가능하기 때문에 주의하자.

🔊 데시벨

오디오 믹서의 볼륨은 데시벨(decibel, db)로 관리된다. 데시벨의 범위는 -80 ~ 0 .
데시벨은 상용로그( log10log_{10} )의 로그 스케일로 표현된다. 그래서 데시벨이 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


스크립트 상에서 믹서의 볼륨에 접근하려면, 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에서 값 가져오기

이제 슬라이더 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;
    }
    
}

💡 PlayerPrefs

간단하게 설정 값을 로컬에 저장하는 용도로 쓸 수 있는 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초정도의 딜레이가 생기게 된다.

해결방법

  1. LoadType 변경 : Streaming으로 로드 방법을 변경하면 한 번에 음원을 다 불러오는 것이 아니라 조금조금씩 가져오는 방식이다.
  2. Load In Background : 백그라운드에서 미리 로딩하는 방법이다.
  3. Quality : 퀄리티를 낮춰도 생각보다 우리의 귀는 그 차이를 잘 모른다. Kbps를 낮추는 방법이다.
  4. Sample Rate Setting : 샘플레이트를 낮추는 것도 방법이다. 일반적으로 48,000 또는 44100이 기본값인데, 여기서 낮추면 된다.

    아래와 같이 세팅할 경우 50메가가 넘던 파일이 4메가까지 줄어든다
profile
개발 박살내자

0개의 댓글