Unity 보이스챗 오디오 Ducking 구현하기: BGM·SFX를 자연스럽게 제어하는 상태 머신

Unity AudioMixer와 C# 상태 머신을 활용해 보이스챗 입력 시 BGM과 효과음을 자연스럽게 감쇠(Ducking)시키는 방법을 다룹니다. VAD 임계값, Attack·Release, Hold 시간을 적용해 음성 전달력과 몰입감을 함께 확보합니다.

✓ 복사 완료!
챕터시스템 아키텍처 & 패턴
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
Unity 보이스챗 오디오 Ducking 구현하기: BGM·SFX를 자연스럽게 제어하는 상태 머신

핵심 요약

보이스챗의 VAD 신호에 맞춰 AudioMixer의 BGM·효과음 버스 볼륨을 dB 단위로 부드럽게 감쇠시키면 음성이 시작될 때 게임 사운드를 자연스럽게 낮출 수 있습니다.

VAD는 Voice Activity Detection(음성 활동 감지)의 약자입니다. 오디오 입력 신호에서 배경 소음이나 무음 구간을 제외하고 실제 사람의 음성이 존재하는 구간만을 감지하고 구분해 내는 기술을 의미합니다.

Idle → DuckingIn → Ducked → DuckingOut 상태 머신과 Attack, Release, Hold 파라미터를 분리하면 마이크 노이즈나 짧은 무음 구간 때문에 볼륨이 요동치는 현상(Pumping)을 효과적으로 방지할 수 있습니다.

보이스챗 오디오 Ducking이 필요한 이유

게임 내 보이스챗 음성과 인게임 사운드가 동일한 볼륨으로 재생되면 팀원의 브리핑을 놓치기 쉽습니다. 그렇다고 BGM과 효과음을 완전히 음소거해 버리면 공간감이 깨져 몰입감이 크게 떨어집니다.

오디오 Ducking은 음성이 감지될 때 다른 사운드의 볼륨을 일시적으로 적정 수준 낮추는 기술입니다. 핵심 파라미터는 다음과 같습니다.

요소역할권장 시작값
Duck LevelBGM·효과음을 낮출 감쇠 목표치-12 dB
AttackDucking 시작 후 목표 볼륨에 도달하기까지 걸리는 시간0.08초
Release음성이 끝난 뒤 원래 볼륨으로 복귀하기까지 걸리는 시간0.25초
Hold발화 사이의 짧은 무음 구간에서도 Ducking을 유지하는 지연 시간0.12초

dB(데시벨) 기준으로 감쇠값을 제어해야 하는 이유

Unity AudioMixer의 파라미터는 데시벨(dB) 스케일로 동작합니다. 선형 볼륨(Gain)과 데시벨의 관계는 다음과 같습니다.

g=10dB20g = 10^{\frac{dB}{20}}

-12 dB는 선형 진폭 기준으로 약 0.25배(1/4 수준)입니다. 인간의 청각은 소리의 크기 변화를 로그 스케일로 인지하기 때문에 0.25 같은 선형 값을 직접 다루는 것보다 AudioMixer의 dB 파라미터를 조절하는 편이 청각적으로 훨씬 균일하고 직관적인 감쇠감을 제공합니다.

AudioMixer 버스 라우팅 구성

먼저 인게임 오디오와 보이스챗을 독립된 믹서 그룹으로 분리해야 합니다.

  1. Music 그룹을 생성해 BGM을 연결합니다.
  2. SFX 그룹을 생성해 게임 효과음을 연결합니다.
  3. 상위 그룹인 DuckingBus를 만들고 Music과 SFX를 그 자식 그룹으로 배치합니다.
  4. VoiceChat 그룹은 감쇠 대상이 아니므로 DuckingBus 바깥(최상위 Master 바로 아래)에 둡니다.
  5. DuckingBus의 Volume 속성을 우클릭하여 GameplayAudioVolume이라는 이름으로 외부에 노출(Expose)합니다.

이 설정을 거치면 개별 AudioSource를 일일이 탐색해 볼륨을 바꿀 필요 없이, 상위 믹서 파라미터 하나만으로 전체 인게임 사운드를 제어할 수 있습니다.

AudioMixer 라우팅 구성 예시

Ducking 상태 머신 설계

마이크 입력 신호는 잡음 제거나 패킷 손실, 발화 중간의 쉼표 구간 때문에 순간적으로 꺼졌다 켜질 수 있습니다. 단순한 bool isSpeaking 플래그로 볼륨을 켜고 끄면 소리가 끊기듯 울렁거리므로 상태와 시간을 함께 관리하는 상태 머신이 적합합니다.

상태설명목표 볼륨
Idle발화가 없어 게임 오디오가 기본 볼륨을 유지하는 상태0 dB
DuckingIn음성이 감지되어 오디오 볼륨을 낮추는 과도 상태 (Attack)Duck Level
Ducked목표 감쇠값에 도달해 유지되는 상태Duck Level
DuckingOut발화 종료 후 원래 볼륨으로 복귀하는 과도 상태 (Release)0 dB
flowchart LR
    A[Idle<br/>0 dB] -->|VAD 음성 감지| B[DuckingIn<br/>Attack]
    B --> C[Ducked<br/>Duck Level]
    C -->|발화 종료 후 Hold 만료| D[DuckingOut<br/>Release]
    D --> A
    D -->|복귀 중 음성 재감지| B

발화가 끝나더라도 즉시 DuckingOut으로 넘어가지 않고 설정한 Hold 시간만큼 대기합니다. 덕분에 단어 사이의 미세한 무음 구간에서도 오디오가 급격히 커졌다가 다시 줄어드는 펌핑 현상을 방지합니다.

C# 상태 머신 구현

외부 보이스챗 SDK의 콜백이나 마이크 RMS 레벨을 전달받아 동작하는 단일 컴포넌트 예제입니다.

using UnityEngine;
using UnityEngine.Audio;

public sealed class VoiceChatDucking : MonoBehaviour
{
    private enum DuckState
    {
        Idle,
        DuckingIn,
        Ducked,
        DuckingOut
    }

    [Header("Audio Mixer Settings")]
    [SerializeField] private AudioMixer mixer;
    [SerializeField] private string gameplayAudioParameter = "GameplayAudioVolume";

    [Header("Ducking Tuning")]
    [SerializeField] private float duckDb = -12f;
    [SerializeField] private float attackSeconds = 0.08f;
    [SerializeField] private float releaseSeconds = 0.25f;
    [SerializeField] private float voiceHoldSeconds = 0.12f;
    [SerializeField] private float voiceThreshold = 0.02f;

    private DuckState state = DuckState.Idle;
    private float currentDb;
    private float targetDb;
    private float smoothVelocity;
    private float holdUntil;
    private bool voiceActive;

    private void Awake()
    {
        currentDb = 0f;
        targetDb = 0f;
        ApplyDb(currentDb);
    }

    private void Update()
    {
        // 음성이 활성화되어 있거나 Hold 유지 시간 이내라면 감쇠 유지
        bool shouldDuck = voiceActive || Time.unscaledTime < holdUntil;

        if (shouldDuck)
        {
            EnterDuck();
        }
        else
        {
            EnterRelease();
        }

        // 감쇠 중이면 Attack 시간을 복귀 중이면 Release 시간을 적용
        float smoothTime = targetDb < currentDb ? attackSeconds : releaseSeconds;

        currentDb = Mathf.SmoothDamp(
            currentDb,
            targetDb,
            ref smoothVelocity,
            smoothTime,
            Mathf.Infinity,
            Time.unscaledDeltaTime);

        // 목표 dB 근처 도달 시 스냅 및 상태 전환
        if (Mathf.Abs(currentDb - targetDb) <= 0.01f)
        {
            currentDb = targetDb;

            if (state == DuckState.DuckingIn)
            {
                state = DuckState.Ducked;
            }
            else if (state == DuckState.DuckingOut)
            {
                state = DuckState.Idle;
            }
        }

        ApplyDb(currentDb);
    }

    public void OnVoicePacketLevel(float rms)
    {
        SetVoiceActivity(rms >= voiceThreshold);
    }

    public void SetVoiceActivity(bool active)
    {
        voiceActive = active;

        if (active)
        {
            holdUntil = Time.unscaledTime + voiceHoldSeconds;
            EnterDuck();
        }
    }

    private void EnterDuck()
    {
        if (state == DuckState.DuckingIn || state == DuckState.Ducked)
        {
            return;
        }

        state = DuckState.DuckingIn;
        targetDb = duckDb;
        smoothVelocity = 0f;
    }

    private void EnterRelease()
    {
        if (state == DuckState.DuckingOut || state == DuckState.Idle)
        {
            return;
        }

        state = DuckState.DuckingOut;
        targetDb = 0f;
        smoothVelocity = 0f;
    }

    private void ApplyDb(float db)
    {
        if (mixer != null)
        {
            mixer.SetFloat(gameplayAudioParameter, db);
        }
    }
}

구현 상세 핵심 포인트

  • Time.unscaledDeltaTime 사용: 인게임 일시정지(Time.timeScale = 0)나 메뉴 화면 진입 상태에서도 보이스챗 대화에 맞춰 사운드 감쇠가 정상 작동하도록 비동기 스케일 시간을 적용했습니다.
  • Mathf.SmoothDamp 보간: 감쇠할 때(Attack)와 원복할 때(Release)의 전환 속도를 비대칭으로 처리했습니다. 일반적으로 소리를 낮출 때는 빠르게, 원복할 때는 완만하게 돌아오는 구조가 귀에 가장 편안합니다.
  • 파라미터 바인딩 확인: AudioMixer.SetFloat는 노출된 파라미터 명칭이 일치하지 않아도 에러를 던지지 않고 무시됩니다. 문자열 키값이 정확한지 반드시 확인해야 합니다.

VAD 신호 연동 및 다중 화자 처리

SDK가 음성 시작/종료 이벤트를 지원한다면 직관적으로 바인딩할 수 있습니다.

voiceChat.OnSpeakingStarted += () => ducking.SetVoiceActivity(true);
voiceChat.OnSpeakingStopped += () => ducking.SetVoiceActivity(false);

다중 화자(Multi-Speaker) 대응

여러 플레이어가 동시에 대화할 수 있는 게임에서는 한 명이 말을 끝냈다고 즉시 오디오를 복귀시키면 안 됩니다. 이벤트 누락을 방지하기 위해 HashSet으로 현재 말하고 있는 화자 목록을 추적하는 방식을 권장합니다.

private readonly HashSet<string> activeSpeakers = new();

public void OnRemoteSpeakerStarted(string playerId)
{
    activeSpeakers.Add(playerId);
    ducking.SetVoiceActivity(activeSpeakers.Count > 0);
}

public void OnRemoteSpeakerStopped(string playerId)
{
    activeSpeakers.Remove(playerId);
    ducking.SetVoiceActivity(activeSpeakers.Count > 0);
}

장르별 튜닝 가이드

게임 장르Duck LevelAttackRelease
FPS / 배틀로얄 (브리핑 우선)-14 ~ -18 dB0.04 ~ 0.08초0.20 ~ 0.35초
협동 RPG / 캐주얼 (자연스러운 대화)-8 ~ -12 dB0.08 ~ 0.15초0.25 ~ 0.50초
BGM 중심 캐주얼 / 힐링 게임-6 ~ -10 dB0.12 ~ 0.20초0.40 ~ 0.70초
  • Attack이 너무 길면: 발화 첫 음절이 게임 사운드에 묻힙니다.
  • Attack이 너무 짧으면: 총성이나 폭발음이 급격히 줄어들어 끊기는 느낌을 줍니다.
  • Release가 너무 짧으면: 말하는 중간중간 소리가 커졌다 줄어드는 펌핑이 발생합니다. Hold를 100~150ms로 잡고 Release를 250~400ms 선에서 조율하는 것이 안정적입니다.

체크리스트

  • 마이크 시작 시 음성의 첫 음절이 묻히지 않고 즉시 감쇠되는가?
  • 문장 사이의 50~100ms 무음 구간에서 볼륨이 요동치지 않는가?
  • 발화 종료 후 원래 볼륨으로 부드럽게 복귀하는가?
  • Time.timeScale = 0 상태에서도 Ducking 전환이 멈추지 않는가?
  • 여러 명이 동시에 말할 때 한 명의 발화 종료로 전체 Ducking이 풀리지 않는가?
  • VoiceChat 버스가 감쇠 대상 부모 버스에 포함되지 않았는가?

프로파일링에서는 매 프레임의 AudioMixer.SetFloat 호출 자체보다 오디오 라우팅과 VAD 이벤트 빈도를 먼저 확인합니다. 파라미터 값이 실제로 바뀌는 경우에만 적용하도록 최적화할 수도 있습니다. 예를 들어 현재 dB와 새 dB의 차이가 0.01 dB 미만이면 호출을 생략하면 불필요한 갱신을 줄일 수 있습니다.

자주 묻는 질문 (FAQ)

보이스챗을 AudioMixer에서 별도 그룹으로 분리해야 하나요?

네. 보이스챗을 VoiceChat 그룹으로 분리하고 BGM·효과음만 DuckingBus 아래에 배치해야 음성 자체가 함께 작아지는 문제를 피할 수 있습니다.

음성이 짧게 끊길 때 볼륨이 계속 흔들리는 이유는 무엇인가요?

VAD 신호가 짧은 무음마다 종료로 판정되기 때문입니다. Hold 시간을 두고 Release를 Attack보다 길게 설정하면 전환 빈도와 펌핑 현상을 줄일 수 있습니다.

AudioMixer.SetFloat 대신 AudioSource 볼륨을 바꿔도 되나요?

가능하지만 재생 중인 모든 BGM·효과음 소스를 찾아 갱신해야 합니다. AudioMixer 그룹을 사용하면 라우팅된 오디오를 한 번에 제어할 수 있어 오브젝트 수가 많은 장면에서 더 관리하기 쉽습니다.

정리

보이스챗 Ducking은 단순히 음량을 즉시 낮추는 기능이 아니라 VAD 신호를 안정적인 오디오 상태로 변환하는 문제입니다. Unity에서는 BGM·효과음을 DuckingBus로 묶고 AudioMixer.SetFloat로 dB 목표값을 제어한 뒤 Idle, DuckingIn, Ducked, DuckingOut 상태와 Attack·Release·Hold 시간을 조합하면 자연스럽고 튜닝 가능한 구현을 만들 수 있습니다.