> ## Documentation Index
> Fetch the complete documentation index at: https://devdocs.xbox.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 실시간 오디오 조작

> XBOX GDK 타이틀의 오디오 조작 파이프라인을 사용하여 Game Chat 2 음성 스트림을 검사하고 실시간 오디오 효과를 적용하는 방법에 대한 안내입니다.

이 항목에서는 실시간 오디오 조작 사용에 대한 간단한 안내를 제공합니다.

Game Chat 2는 채팅 오디오 파이프라인에 삽입하여 사용자의 채팅 오디오 데이터를 검사하고 조작할 수 있는 옵션을 제공합니다.
이는 게임 내에서 사용자의 음성에 흥미로운 오디오 효과를 적용하는 데 유용할 수 있습니다.

Game Chat 2에서 오디오 조작 파이프라인은 오디오 데이터를 폴링할 수 있는 오디오 스트림 개체를 통해 상호 작용합니다.
콜백을 사용하는 것과 달리, 이 모델을 사용하여 가장 편리한 처리 스레드에서 오디오를 검사하거나 조작할 수 있습니다.

## 오디오 조작 파이프라인 초기화

기본적으로 Game Chat 2는 실시간 오디오 조작을 활성화하지 않습니다.
이를 활성화하려면 앱이 `audioManipulationMode` 매개변수를 설정하여 [chat\_manager::initialize](/reference/chat/gamechat2/classes/chat_manager/methods/chat_manager_initialize)에서 활성화할 오디오 조작 형식을 지정해야 합니다.

현재 다음과 같은 오디오 조작 형식이 지원되며 [game\_chat\_audio\_manipulation\_mode\_flags](/reference/chat/gamechat2/enums/game_chat_audio_manipulation_mode_flags) 열거형에 있습니다.

* `game_chat_audio_manipulation_mode_flags::none`: 오디오 조작을 비활성화합니다. 이것이 기본 구성입니다. 이 모드에서는 채팅 오디오가 중단 없이 흐릅니다.
* `game_chat_audio_manipulation_mode_flags::pre_encode_stream_manipulation`: 인코딩 전 오디오 조작을 활성화합니다. 이 모드에서는 로컬 사용자가 생성한 모든 채팅 오디오가 인코딩되기 전에 오디오 조작 파이프라인을 통해 공급됩니다. 앱이 채팅 오디오 데이터를 조작하지 않고 검사만 하더라도, 오디오 버퍼가 인코딩되고 전송될 수 있도록 변경되지 않은 오디오 버퍼를 Game Chat 2에 다시 제출하는 것은 여전히 앱의 책임입니다.
* `game_chat_audio_manipulation_mode_flags::post_decode_stream_manipulation`: 디코딩 후 오디오 조작을 활성화합니다. 이 모드에서는 원격 사용자로부터 수신된 모든 채팅 오디오가 수신자에 의해 디코딩된 후 렌더링되기 전에 오디오 조작 파이프라인을 통해 공급됩니다. 앱이 채팅 오디오 데이터를 조작하지 않고 검사만 하더라도, 오디오 버퍼가 렌더링될 수 있도록 변경되지 않은 오디오 버퍼를 믹싱하고 Game Chat 2에 다시 제출하는 것은 여전히 앱의 책임입니다.

## 오디오 스트림 상태 변경 처리

Game Chat 2는 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 오디오 스트림 상태에 대한 업데이트를 제공합니다.
이러한 업데이트는 어떤 스트림이 업데이트되었고 어떻게 업데이트되었는지에 대한 정보를 저장합니다.

이러한 업데이트는 [chat\_manager::start\_processing\_stream\_state\_changes()](/reference/chat/gamechat2/classes/chat_manager/methods/chat_manager_start_processing_stream_state_changes) 및 [chat\_manager::finish\_processing\_stream\_state\_changes()](/reference/chat/gamechat2/classes/chat_manager/methods/chat_manager_finish_processing_stream_state_changes) 메서드 쌍에 대한 호출을 통해 폴링될 수 있습니다. 이 메서드는 최신 대기 오디오 스트림 상태 업데이트를 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체 포인터 배열로 제공합니다. 앱은 배열을 반복하고 각 업데이트를 적절하게 처리해야 합니다.

사용 가능한 모든 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 업데이트가 처리된 후 해당 배열은 `chat_manager::finish_processing_stream_state_changes()`를 통해 Game Chat 2로 다시 전달되어야 합니다.
다음 예제에 표시되어 있습니다.

```cpp theme={null}
uint32_t streamStateChangeCount;
game_chat_stream_state_change_array streamStateChanges;
chat_manager::singleton_instance().start_processing_stream_state_changes(&streamStateChangeCount, &streamStateChanges);

for (uint32_t streamStateChangeIndex = 0; streamStateChangeIndex < streamStateChangeCount; ++streamStateChangeIndex)
{
    switch (streamStateChanges[streamStateChangeIndex]->state_change_type)
    {
        case game_chat_stream_state_change_type::pre_encode_audio_stream_created:
        {
            HandlePreEncodeAudioStreamCreated(streamStateChanges[streamStateChangeIndex].pre_encode_audio_stream);
            break;
        }

        case Xs::game_chat_2::game_chat_stream_state_change_type::pre_encode_audio_stream_closed:
        {
            HandlePreEncodeAudioStreamClosed(streamStateChanges[streamStateChangeIndex].pre_encode_audio_stream);
            break;
        }

        ...
    }
}
chat_manager::singleton_instance().finish_processing_stream_state_changes(streamStateChanges);
```

## 인코딩 전 채팅 오디오 데이터 조작

Game Chat 2는 [pre\_encode\_audio\_stream](/reference/chat/gamechat2/classes/pre_encode_audio_stream/pre_encode_audio_stream) 클래스를 통해 로컬 사용자에 대한 인코딩 전 채팅 오디오 데이터에 대한 액세스를 제공합니다.

### 스트림 수명

새 [pre\_encode\_audio\_stream](/reference/chat/gamechat2/classes/pre_encode_audio_stream/pre_encode_audio_stream) 인스턴스가 앱에서 사용할 준비가 되면 `state_change_type` 필드가 `game_chat_stream_state_change_type::pre_encode_audio_stream_created`로 설정된 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 전달됩니다.
이 스트림 상태 변경이 Game Chat 2에 반환되면 오디오 스트림은 인코딩 전 오디오 조작에 사용할 수 있게 됩니다.

기존 [pre\_encode\_audio\_stream](/reference/chat/gamechat2/classes/pre_encode_audio_stream/pre_encode_audio_stream)이 오디오 조작에 사용할 수 없게 되면, `state_change_type` 필드가 `game_chat_stream_state_change_type::pre_encode_audio_stream_closed`로 설정된 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 앱에 알림이 전송됩니다.
이는 앱이 이 오디오 스트림과 관련된 리소스 정리를 시작할 수 있는 기회입니다.
이 스트림 상태 변경이 Game Chat 2에 반환되면 오디오 스트림은 인코딩 전 오디오 조작에 사용할 수 없게 됩니다.

닫힌 [pre\_encode\_audio\_stream](/reference/chat/gamechat2/classes/pre_encode_audio_stream/pre_encode_audio_stream)의 모든 리소스가 반환되면, 스트림이 소멸되고 `state_change_type` 필드가 `game_chat_stream_state_change_type::pre_encode_audio_stream_destroyed`로 설정된 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 앱에 알림이 전송됩니다.
이 스트림에 대한 참조 또는 포인터는 정리되어야 합니다.
이 스트림 상태 변경이 Game Chat 2에 반환되면 오디오 스트림 메모리가 유효하지 않게 됩니다.

### 스트림 사용자

스트림과 관련된 사용자 목록은 [pre\_encode\_audio\_stream::get\_users()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_get_users)를 사용하여 검사할 수 있습니다.

### 오디오 형식

앱이 Game Chat 2에서 검색하는 버퍼의 오디오 형식은 [pre\_encode\_audio\_stream::get\_pre\_processed\_format()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_get_pre_processed_format)을 사용하여 검사할 수 있습니다. 사전 처리된 오디오 형식은 모노입니다.
앱은 32비트 부동 소수점, 16비트 정수 및 32비트 정수로 표현된 데이터를 처리할 것으로 예상해야 합니다.

앱은 [pre\_encode\_audio\_stream::set\_processed\_format()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_set_processed_format)을 사용하여 인코딩 및 전송을 위해 제출되는 조작된 버퍼의 오디오 형식을 Game Chat 2에 알려야 합니다. 인코딩 전 오디오 스트림에 대한 처리된 형식은 다음과 같은 사전 조건을 충족해야 합니다.

* 형식은 모노여야 합니다.
* 형식은 32비트 부동 소수점 Performance Monitor 카운터(PCM), 32비트 정수 PCM 또는 16비트 정수 PCM 형식이어야 합니다.
* 형식의 샘플 속도는 플랫폼에 따른 사전 조건을 따라야 합니다. XBOX One ERA 및 XBOX Series X|S는 8KHz, 12KHz, 16KHz 및 24KHz 샘플 속도를 지원합니다. XBOX One 및 Windows PC용 Universal Windows Platform(UWP)은 8KHz, 12KHz, 16KHz, 24KHz, 32KHz, 44.1KHz 및 48KHz 샘플 속도를 지원합니다.

### 인코딩 전 오디오 검색 및 제출

앱은 [pre\_encode\_audio\_stream::get\_available\_buffer\_count()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_get_available_buffer_count)를 사용하여 처리할 수 있는 사용 가능한 버퍼 수에 대해 인코딩 전 오디오 스트림을 쿼리할 수 있습니다. 이 정보는 최소 개수의 버퍼가 사용 가능해질 때까지 오디오 처리를 지연시키려는 경우 사용할 수 있습니다.

각 인코딩 전 오디오 스트림에는 10개의 버퍼만 대기하며, 오디오 지연은 오디오 파이프라인에 지연 시간을 도입합니다. 앱이 4개 이상의 버퍼를 대기하기 전에 인코딩 전 오디오 스트림을 배출하는 것이 좋습니다.

#### get\_next\_buffer()를 사용하여 오디오 버퍼 검색

앱은 [pre\_encode\_audio\_stream::get\_next\_buffer()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_get_next_buffer)를 사용하여 인코딩 전 오디오 스트림에서 오디오 버퍼를 검색할 수 있습니다.
새 오디오 버퍼는 평균적으로 40ms마다 한 번 사용할 수 있습니다.

이 메서드에서 반환된 버퍼는 사용이 완료되면 [pre\_encode\_audio\_stream::return\_buffer()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_return_buffer)로 해제해야 합니다.

인코딩 전 오디오 스트림에는 언제든지 최대 10개의 대기 또는 반환되지 않은 버퍼가 존재할 수 있습니다.
이 한도에 도달하면 사용자의 오디오 소스에서 캡처된 새 버퍼는 미해결 버퍼 중 일부가 반환될 때까지 삭제됩니다.

#### submit\_buffer()를 사용하여 오디오 버퍼 제출

앱은 [pre\_encode\_audio\_stream::submit\_buffer()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_submit_buffer)를 사용하여 검사되고 조작된 오디오 버퍼를 인코딩 및 전송을 위해 Game Chat 2에 다시 제출할 수 있습니다. Game Chat 2는 제자리 및 비제자리 오디오 조작을 지원합니다. `pre_encode_audio_stream::submit_buffer()`에 제출된 버퍼가 반드시 `pre_encode_audio_stream::get_next_buffer()`에서 검색된 버퍼와 동일한 버퍼일 필요는 없습니다.

이러한 제출된 버퍼에 대한 개인정보 보호/권한은 이 스트림과 관련된 사용자를 기반으로 적용됩니다.
40ms마다 이 스트림의 다음 40ms 오디오가 인코딩되고 전송됩니다.

오디오 끊김을 방지하려면 지속적으로 들리도록 해야 하는 오디오에 대한 버퍼는 일정한 속도로 이 스트림에 제출되어야 합니다.

### 스트림 컨텍스트

앱은 [pre\_encode\_audio\_stream::set\_custom\_stream\_context()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_set_custom_stream_context) 및 [pre\_encode\_audio\_stream::custom\_stream\_context()](/reference/chat/gamechat2/classes/pre_encode_audio_stream/methods/pre_encode_audio_stream_custom_stream_context)를 사용하여 인코딩 전 오디오 스트림에서 사용자 지정 포인터 크기의 컨텍스트 값을 관리할 수 있습니다. 이러한 사용자 지정 스트림 컨텍스트는 Game Chat 2 오디오 스트림과 보조 데이터 간의 매핑을 생성하는 데 유용합니다. 예를 들면, 스트림 메타데이터 및 게임 상태입니다.

### 예제

다음은 하나의 오디오 처리 프레임에서 인코딩 전 오디오 스트림을 사용하는 방법에 대한 단순화된 종단 간 샘플입니다.

```cpp theme={null}
uint32_t streamStateChangeCount;
game_chat_stream_state_change_array streamStateChanges;
chat_manager::singleton_instance().start_processing_stream_state_changes(&streamStateChangeCount, &streamStateChanges);

for (uint32_t streamStateChangeIndex = 0; streamStateChangeIndex < streamStateChangeCount; ++streamStateChangeIndex)
{
    switch (streamStateChanges[streamStateChangeIndex]->state_change_type)
    {
        case game_chat_stream_state_change_type::pre_encode_audio_stream_created:
        {
            pre_encode_audio_stream* stream = streamStateChanges[streamStateChangeIndex]->pre_encode_audio_stream;
            stream->set_processed_audio_format(...);
            stream->set_custom_stream_context(...);
            HandlePreEncodeAudioStreamCreated(stream);
            break;
        }

        case game_chat_2::game_chat_stream_state_change_type::pre_encode_audio_stream_closed:
        {
            HandlePreEncodeAudioStreamClosed(streamStateChanges[streamStateChangeIndex].pre_encode_audio_stream);
            break;
        }

        case game_chat_2::game_chat_stream_state_change_type::pre_encode_audio_stream_destroyed:
        {
            HandlePreEncodeAudioStreamDestroyed(streamStateChanges[streamStateChangeIndex].pre_encode_audio_stream);
            break;
        }

        ...
    }
}
chat_manager::singleton_instance().finish_processing_stream_state_changes(streamStateChanges);

uint32_t preEncodeAudioStreamCount;
pre_encode_audio_stream_array preEncodeAudioStreams;
chat_manager::singleton_instance().get_pre_encode_audio_streams(&preEncodeAudioStreamCount, &preEncodeAudioStreams);
for (uint32_t preEncodeAudioStreamIndex = 0; preEncodeAudioStreamIndex < preEncodeAudioStreamCount; ++preEncodeAudioStreamIndex)
{
    pre_encode_audio_stream* stream = preEncodeAudioStreams[preEncodeAudioStreamIndex];
    StreamContext* context = reinterpret_cast<StreamContext*>(stream->custom_stream_context());

    game_chat_audio_format audio_format = stream->get_pre_processed_format();

    uint32_t preProcessedBufferByteCount;
    void* preProcessedBuffer;
    stream->get_next_buffer(&preProcessedBufferByteCount, &preProcessedBuffer);

    while (preProcessedBuffer != nullptr)
    {
        void* processedBuffer = nullptr;
        switch (audio_format.bits_per_sample)
        {
            case 16:
            {
                assert (audio_format.sample_type == game_chat_sample_type::integer);
                processedBuffer = ManipulateChatBuffer<int16_t>(preProcessedBufferByteCount, preProcessedBuffer, context);
                break;
            }

            case 32:
            {
                switch (audio_format.sample_type)
                {
                    case game_chat_sample_type::integer:
                    {
                        processedBuffer = ManipulateChatBuffer<int32_t>(preProcessedBufferByteCount, preProcessedBuffer, context);
                        break;
                    }

                    case game_chat_sample_type::ieee_float:
                    {
                        processedBuffer = ManipulateChatBuffer<float>(preProcessedBufferByteCount, preProcessedBuffer, context);
                        break;
                    }

                    default:
                    {
                        assert(false);
                        break;
                    }
                }
                break;
            }

            default:
            {
                assert(false);
                break;
            }
        }
        // processedBuffer can be the same as preProcessedBuffer (in-place manipulation) or it can be a buffer of
        // memory not managed by Game Chat 2 (out-of-place manipulation).
        stream->submit_buffer(processedBuffer);
        // Only return buffers retrieved from Game Chat 2. Don't return foreign memory to return_buffer.
        stream->return_buffer(preProcessedBuffer);
        stream->get_next_buffer(&preProcessedBufferByteCount, &preProcessedBuffer);
    }
}

Sleep(audioProcessingPeriodInMilliseconds);
```

## 디코딩 후 채팅 오디오 데이터 조작

Game Chat 2는 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream) 및 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream) 클래스를 통해 디코딩 후 채팅 오디오 데이터에 대한 액세스를 제공합니다. 이는 사용자가 채팅 오디오의 각 로컬 수신자에 대해 원격 사용자의 오디오를 고유하게 조작할 수 있음을 의미합니다.

### 소스 및 싱크

인코딩 전 파이프라인과 달리 디코딩 후 오디오 데이터를 처리하는 모델은 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream)과 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream)의 두 클래스로 분할됩니다.

원격 사용자로부터 디코딩된 오디오는 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream) 개체에서 검색되고 조작되어 렌더링을 위해 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream) 개체로 전송될 수 있습니다.
이를 통해 Game Chat 2 디코딩 후 오디오 처리 파이프라인과 유용한 오디오 미들웨어 간의 통합이 가능합니다.

### 스트림 수명

새 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream) 또는 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream) 인스턴스가 앱에서 사용할 준비가 되면 각각 `state_change_type` 필드가 `game_chat_stream_state_change_type::post_decode_audio_source_stream_created` 또는 `game_chat_stream_state_change_type::post_decode_audio_sink_stream_created`로 설정된 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 전달됩니다.
이 스트림 상태 변경이 Game Chat 2에 반환되면 오디오 스트림은 디코딩 후 오디오 조작에 사용할 수 있게 됩니다.

기존 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream) 또는 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream)이 오디오 조작에 사용할 수 없게 되면, 각각 `state_change_type` 필드가 `game_chat_stream_state_change_type::post_decode_audio_source_stream_closed` 또는 `game_chat_stream_state_change_type::post_decode_audio_sink_stream`으로 설정된 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 앱에 알림이 전송됩니다.
이는 앱이 이 오디오 스트림과 관련된 리소스 정리를 시작할 수 있는 기회입니다.
이 스트림 상태 변경이 Game Chat 2에 반환되면 오디오 스트림은 디코딩 후 오디오 조작에 사용할 수 없게 됩니다.
소스 스트림의 경우 이는 더 이상 조작을 위해 대기 중인 버퍼가 없음을 의미합니다.
싱크 스트림의 경우 이는 제출된 버퍼가 더 이상 렌더링되지 않음을 의미합니다.

닫힌 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream) 또는 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream)의 모든 리소스가 반환되면, 스트림이 소멸되고 각각 `state_change_type` 필드가 `game_chat_stream_state_change_type::post_decode_audio_source_stream_destroyed` 또는 `game_chat_stream_state_change_type::post_decode_audio_sink_stream_destroyed`로 설정된 [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change) 구조체를 통해 앱에 알림이 전송됩니다.
이 스트림에 대한 참조 또는 포인터는 정리되어야 합니다.
이 스트림 상태 변경이 Game Chat 2에 반환되면 오디오 스트림 메모리가 유효하지 않게 됩니다.

### 스트림 사용자

디코딩 후 소스 스트림과 관련된 원격 사용자 목록은 [post\_decode\_audio\_source\_stream::get\_users()](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_get_users)를 사용하여 검사할 수 있습니다.

디코딩 후 싱크 스트림과 관련된 로컬 사용자 목록은 [post\_decode\_audio\_sink\_stream::get\_users()](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/methods/post_decode_audio_sink_stream_get_users)를 사용하여 검사할 수 있습니다.

### 오디오 형식

앱이 Game Chat 2에서 검색하는 버퍼의 오디오 형식은 [post\_decode\_audio\_source\_stream::get\_pre\_processed\_format()](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_get_pre_processed_format)을 사용하여 검사할 수 있습니다. 사전 처리된 오디오 형식은 항상 모노, 16비트 정수 PCM입니다.

앱은 [post\_decode\_audio\_sink\_stream::set\_processed\_format()](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/methods/post_decode_audio_sink_stream_set_processed_format)을 사용하여 렌더링을 위해 제출되는 조작된 버퍼의 오디오 형식을 Game Chat 2에 알려야 합니다. 디코딩 후 오디오 싱크 스트림에 대한 처리된 형식은 다음 사전 조건을 충족해야 합니다.

* 형식은 64개 미만의 채널을 가져야 합니다.
* 형식은 16비트 정수 PCM(최적), 20비트 정수 PCM(24비트 컨테이너 내), 24비트 정수 PCM, 32비트 정수 PCM 또는 32비트 부동 소수점 PCM(16비트 정수 PCM 다음으로 선호되는 형식)이어야 합니다.
* 형식의 샘플 속도는 초당 1,000\~200,000 샘플 사이여야 합니다.

### 인코딩 후 오디오 검색 및 제출

앱은 [post\_decode\_audio\_source\_stream::get\_available\_buffer\_count()](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_get_available_buffer_count)를 사용하여 처리할 수 있는 사용 가능한 버퍼 수에 대해 디코딩 후 오디오 소스 스트림을 쿼리할 수 있습니다.
이 정보는 최소 개수의 버퍼가 사용 가능해질 때까지 오디오 처리를 지연시키려는 경우 사용할 수 있습니다.
각 디코딩 후 오디오 소스 스트림에는 10개의 버퍼만 대기하며, 오디오 지연은 오디오 파이프라인에 지연 시간을 도입합니다. 앱이 4개 이상의 버퍼를 대기하기 전에 디코딩 후 오디오 스트림을 배출하는 것이 좋습니다.

#### get\_next\_buffer()를 사용하여 오디오 버퍼 검색

앱은 [post\_decode\_audio\_source\_stream::get\_next\_buffer()](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_get_next_buffer)를 사용하여 디코딩 후 오디오 소스 스트림에서 오디오 버퍼를 검색할 수 있습니다. 새 오디오 버퍼는 평균적으로 40ms마다 한 번 사용할 수 있습니다.

이 메서드에서 반환된 버퍼는 사용이 완료되면 [post\_decode\_audio\_source\_stream::return\_buffer()](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_return_buffer)로 해제해야 합니다.

디코딩 후 오디오 소스 스트림에는 언제든지 최대 10개의 대기 또는 반환되지 않은 버퍼가 존재할 수 있습니다.
이 한도에 도달하면 원격 사용자로부터의 새로 디코딩된 버퍼는 미해결 버퍼 중 일부가 반환될 때까지 삭제됩니다.

#### submit\_buffer()를 사용하여 오디오 버퍼 제출

앱은 [post\_decode\_audio\_sink\_stream::submit\_mixed\_buffer()](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/methods/post_decode_audio_sink_stream_submit_mixed_buffer)를 사용하여 렌더링을 위해 디코딩 후 오디오 싱크 스트림을 통해 검사되고 조작된 버퍼를 Game Chat 2에 다시 제출할 수 있습니다.
Game Chat 2는 제자리 및 비제자리 오디오 조작을 지원합니다. `post_decode_audio_sink_stream::submit_mixed_buffer()`에 제출된 버퍼가 반드시 [post\_decode\_audio\_source\_stream::get\_next\_buffer()](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_get_next_buffer)에서 검색된 버퍼와 동일한 버퍼일 필요는 없습니다.

40ms마다 이 스트림의 다음 40ms 오디오가 렌더링됩니다.
오디오 끊김을 방지하려면 지속적으로 들리도록 해야 하는 오디오에 대한 버퍼는 일정한 속도로 이 스트림에 제출되어야 합니다.

### 개인정보 보호 및 믹싱

디코딩 후 파이프라인의 소스-싱크 모델 때문에 [post\_decode\_audio\_source\_stream](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/post_decode_audio_source_stream) 개체에서 검색된 버퍼를 믹싱하고 렌더링을 위해 [post\_decode\_audio\_sink\_stream](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/post_decode_audio_sink_stream) 개체에 믹싱된 버퍼를 제출하는 것은 앱의 책임입니다. 이는 또한 적절한 개인정보 보호 및 권한이 적용된 상태에서 믹싱을 수행하는 것도 앱의 책임임을 의미합니다.
Game Chat 2는 이 정보를 간단하고 효율적으로 쿼리할 수 있도록 [post\_decode\_audio\_sink\_stream::can\_receive\_audio\_from\_source\_stream()](/reference/chat/gamechat2/classes/post_decode_audio_sink_stream/methods/post_decode_audio_sink_stream_can_receive_audio_from_source_stream)을 제공합니다.

### 채팅 표시기

디코딩 후 오디오 조작은 각 사용자의 채팅 표시기 상태에 영향을 주지 않습니다.
예를 들어, 원격 사용자가 음소거된 경우 오디오가 앱에 제공됩니다. 그러나 해당 원격 사용자의 채팅 표시기는 여전히 음소거로 표시됩니다.

원격 사용자가 말할 때 오디오가 제공됩니다. 그러나 앱이 해당 사용자의 오디오를 포함하는 오디오 믹스를 제공하는지 여부와 관계없이 채팅 표시기는 말하기로 표시됩니다.
UI 및 채팅 표시기에 대한 자세한 내용은 [Game Chat 2 사용](/services/xbox-services/multiplayer/chat/game-chat2/using-game-chat-2#ui)을 참조하세요.

오디오 믹스에 있는 사용자를 결정하는 데 추가 앱별 제한 사항이 사용되는 경우, Game Chat 2에서 제공하는 채팅 표시기를 읽을 때 동일한 제한 사항을 고려하는 것은 앱의 책임입니다.

### 스트림 컨텍스트

앱은 [post\_decode\_audio\_source\_stream::set\_custom\_stream\_context](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_set_custom_stream_context) 및 [post\_decode\_audio\_source\_stream::custom\_stream\_context](/reference/chat/gamechat2/classes/post_decode_audio_source_stream/methods/post_decode_audio_source_stream_custom_stream_context) 메서드를 사용하여 디코딩 후 오디오 스트림에서 사용자 지정 포인터 크기의 컨텍스트 값을 관리할 수 있습니다.
이러한 사용자 지정 스트림 컨텍스트는 Game Chat 2 오디오 스트림과 보조 데이터 간의 매핑을 생성하는 데 유용합니다. 예를 들면, 스트림 메타데이터 및 게임 상태입니다.

### 예제

다음은 하나의 오디오 처리 프레임에서 디코딩 후 오디오 스트림을 사용하는 방법에 대한 단순화된 종단 간 샘플입니다.

```cpp theme={null}
uint32_t streamStateChangeCount;
game_chat_stream_state_change_array streamStateChanges;
chat_manager::singleton_instance().start_processing_stream_state_changes(&streamStateChangeCount, &streamStateChanges);

for (uint32_t streamStateChangeIndex = 0; streamStateChangeIndex < streamStateChangeCount; ++streamStateChangeIndex)
{
    switch (streamStateChanges[streamStateChangeIndex]->state_change_type)
    {
        case game_chat_stream_state_change_type::post_decode_audio_source_stream_created:
        {
            post_decode_audio_source_stream* stream = streamStateChanges[streamStateChangeIndex]->post_decode_audio_source_stream;
            stream->set_custom_stream_context(...);
            HandlePostDecodeAudioSourceStreamCreated(stream);
            break;
        }

        case game_chat_stream_state_change_type::post_decode_audio_source_stream_closed:
        {
            HandlePostDecodeAudioSourceStreamClosed(stream);
            break;
        }

        case game_chat_stream_state_change_type::post_decode_audio_source_stream_destroyed:
        {
            HandlePostDecodeAudioSourceStreamDestroyed(stream);
            break;
        }

        case game_chat_stream_state_change_type::post_decode_audio_sink_stream_created:
        {
            post_decode_audio_sink_stream* stream = streamStateChanges[streamStateChangeIndex]->post_decode_audio_sink_stream;
            stream->set_custom_stream_context(...);
            stream->set_processed_format(...);
            HandlePostDecodeAudioSinkStreamCreated(stream);
            break;
        }

        case game_chat_stream_state_change_type::post_decode_audio_sink_stream_closed:
        {
            HandlePostDecodeAudioSinkStreamClosed(stream);
            break;
        }

        case game_chat_stream_state_change_type::post_decode_audio_sink_stream_destroyed:
        {
            HandlePostDecodeAudioSinkStreamDestroyed(stream);
            break;
        }

        ...
    }
}

chat_manager::singleton_instance().finish_processing_stream_state_changes(streamStateChanges);

uint32_t sourceStreamCount;
post_decode_audio_source_stream_array sourceStreams;
chatManager::singleton_instance().get_post_decode_audio_source_streams(&sourceStreamCount, &sourceStreams);

uint32_t sinkStreamCount;
post_decode_audio_sink_stream_array sinkStreams;
chatManager::singleton_instance().get_post_decode_audio_sink_streams(&sinkStreamCount, &sinkStreams);

//
// MixBuffer is a custom type defined as:
// struct MixBuffer
// {
//     uint32_t bufferByteCount;
//     void* buffer;
// };
//
std::vector<std::pair<post_decode_audio_source_stream*, MixBuffer>> cachedSourceBuffers;

for (uint32_t sourceStreamIndex = 0; sourceStreamIndex < sourceStreamCount; ++sourceStreamIndex)
{
    post_decode_audio_source_stream* sourceStream = sourceStreams[sourceStreamIndex];

    MixBuffer mixBuffer;
    sourceStream->get_next_buffer(&mixBuffer.bufferByteCount, &mixBuffer.buffer);
    if (buffer != nullptr)
    {
        // Stash the buffer to return after we're done with mixing. If this program was using audio middleware, now
        // would be an appropriate time to plumb the buffer through the middleware.
        cachedSourceBuffer.push_back(std::pair<post_decode_audio_source_stream*, MixBuffer>{sourceStream, mixBuffer});
    }
}

// Loop over each sink stream, perform mixing, and submit.
for (uint32_t sinkStreamIndex = 0; sinkStreamIndex < sinkStreamCount; ++sinkStreamIndex)
{
    post_decode_audio_sink_stream* sinkStream = sinkStreams[sinkStreamIndex];

    if (sinkStream->is_open())
    {
        std::vector<std::pair<MixBuffer, float>> buffersToMixForThisStream;

        for (const std::pair<post_decode_audio_source_stream, MixBuffer>& sourceBufferPair : cachedSourceBuffers)
        {
            float volume;
            if (sinkStream->can_receive_audio_from_source_stream(sourceBufferPair.first, &volume))
            {
                buffersToMixForThisStream.push_back(std::pair<MixBuffer, float>{sourceBufferPair.second, volume});
            }
        }

        if (buffersToMixForThisStream.size() > 0)
        {
            uint32_t mixedBufferByteCount;
            uint8_t* mixedBuffer;
            MixPostDecodeBuffers(buffersToMixForThisStream, &mixedBufferByteCount, &mixedBuffer);
            sinkStream->submit_mixed_buffer(mixedBufferByteCount, mixedBuffer);
        }
    }
}

// Return buffers after mix and submission.
for (const std::pair<post_decode_audio_source_stream*, MixBuffer>& cachedSourceBuffer : cachedSourceBuffers)
{
    post_decode_audio_source_stream* sourceStream = cachedSourceBuffer.first;
    void* bufferToReturn = cachedSourceBuffer.second.buffer;
    sourceStream->return_buffer(bufferToReturn);
}

Sleep(audioProcessingPeriodInMilliseconds);
```

<a id="chat-user-lifetimes" />

## 채팅 사용자 수명

실시간 오디오 조작을 활성화하면 채팅 사용자의 수명에 영향을 줍니다.
[chat\_manager::remove\_user(chatUserX)](/reference/chat/gamechat2/classes/chat_manager/methods/chat_manager_remove_user)가 호출되면 `chatUserX`가 가리키는 [chat\_user](/reference/chat/gamechat2/classes/chat_user/chat_user) 개체는 `chatUserX`를 참조하는 모든 오디오 스트림이 소멸될 때까지 유효한 상태로 유지됩니다.

다음 시나리오를 고려하세요.

```cpp theme={null}
// At some point, a chat user, chatUserX, leaves the game session.
chat_manager::singleton_instance().remove_user(chatUserX);

// chatUserX is still valid, but to avoid further synchronization, prevent non-audio-stream use of chatUserX.
chatUserX = nullptr;

// On the audio processing thread...
uint32_t streamStateChangeCount;
game_chat_stream_state_change_array streamStateChanges;
chat_manager::singleton_instance().start_processing_stream_state_changes(&streamStateChangeCount, &streamStateChanges);
for (uint32_t streamStateChangeIndex = 0; streamStateChangeIndex < streamStateChangeCount; ++streamStateChangeIndex)
{
    switch (streamStateChanges[streamStateChangeIndex]->state_change_type)
    {
        ...

        // All the streams that are associated with chatUserX will close.
        case Xs::game_chat_2::game_chat_stream_state_change_type::pre_encode_audio_stream_closed:
        {
            CleanupPreEncodeAudioStreamResources(streamStateChanges[streamStateChangeIndex].pre_encode_audio_stream);
            break;
        }

        ...
    }
}
chat_manager::singleton_instance().finish_processing_stream_state_changes(streamStateChanges);

// The next time the app processes stream state changes...
uint32_t streamStateChangeCount;
game_chat_stream_state_change_array streamStateChanges;
chat_manager::singleton_instance().start_processing_stream_state_changes(&streamStateChangeCount, &streamStateChanges);
for (uint32_t streamStateChangeIndex = 0; streamStateChangeIndex < streamStateChangeCount; ++streamStateChangeIndex)
{
    switch (streamStateChanges[streamStateChangeIndex]->state_change_type)
    {
        ...

        case Xs::game_chat_2::game_chat_stream_state_change_type::pre_encode_audio_stream_destroyed:
        {
            uint32_t chatUserCount;
            Xs::game_chat_2::chat_user_array chatUsers;
            streamStateChanges[streamStateChangeIndex].pre_encode_audio_stream->get_users(&chatUserCount, &chatUsers);
            assert(chatUserCount != 0);
            for (uint32_t chatUserIndex = 0; chatUserIndex < chatUserCount; ++chatUserIndex)
            {
                // chat_user objects such as chatUserX will still be valid while the destroyed state change is being processed.
                Log(chatUsers[chatUserIndex]->xbox_user_id());
            }
            break;
        }

        ...
    }
}
chat_manager::singleton_instance().finish_processing_stream_state_changes(streamStateChanges);
// After the all destroyed state changes have been processed for all streams associated with chatUserX, its memory will be invalidated.
// Don't call methods on chatUserX. For example, chatUserX->xbox_user_id()
```

## 참조 API 문서

* [Gamechat2 (API 콘텐츠)](/reference/chat/gamechat2/gamechat2_members)
  * 구조체
    * [game\_chat\_stream\_state\_change](/reference/chat/gamechat2/structs/game_chat_stream_state_change)

## 참고 항목

[Game Chat 2 소개](/services/xbox-services/multiplayer/chat/game-chat2/game-chat-2-intro)

[Game Chat 2 C++ API 사용](/services/xbox-services/multiplayer/chat/game-chat2/using-game-chat-2)

[API 콘텐츠(GameChat2)](/reference/chat/gamechat2/gamechat2_members)

[Microsoft Game Development Kit](/services/playfab/sdks/platforms/gdk)
