实时音频操作

本主题简要介绍了如何使用实时音频操作。

游戏聊天 2 允许您将自己插入聊天音频管道中,以检查和操作用户的聊天音频数据。 这对于向游戏中的用户应用有趣的音频效果非常有用。

在游戏聊天 2 中,音频操作管道通过可为获得音频数据而轮询的音频流对象交互。 与使用回调相反的是,您可以使用此模型在最方便他们的任何处理线程上检查或操作音频。

初始化音频操作管道

默认情况下,Game Chat 2 不启用实时音频操作。 若要启用,应用必须通过设置 audioManipulationMode 参数来指定要在 chat_manager::initialize 中启用的音频操作形式。

目前支持以下音频操作形式,可在 game_chat_audio_manipulation_mode_flags enum 中找到。

  • 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 结构为音频流的状态提供更新。 这些更新存储关于哪些流已更新以及更新方式的信息。

这些更新可以通过对 chat_manager::start_processing_stream_state_changes()chat_manager::finish_processing_stream_state_changes() 方法对的调用获得轮询。 它们将所有最新、排队中的音频流状态更新作为一系列 game_chat_stream_state_change 结构指针提供。 应用应迭代该数组,并正确处理每次的更新。

在所有可用的 game_chat_stream_state_change 更新得到处理后,该阵列应通过 chat_manager::finish_processing_stream_state_changes() 传回至 Game Chat 2。 下面的示例对此进行了展示。

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 类为本地用户提供对预编码聊天音频数据的访问。

流生命周期

当新的 pre_encode_audio_stream 实例准备就绪供应用使用时,它将通过 game_chat_stream_state_change 结构来传递,其中该结构的 state_change_type 字段设置为 game_chat_stream_state_change_type::pre_encode_audio_stream_created。 在此流状态更改返回到 Game Chat 2 后,音频流就可以用于预编码音频操作。

现有 pre_encode_audio_stream 不可用于音频操作时,将通过 game_chat_stream_state_change 结构来通知应用,其中该结构的 state_change_type 字段设置为 game_chat_stream_state_change_type::pre_encode_audio_stream_closed。 这时应用有机会开始清理与此音频流相关联的资源。 在此流状态更改返回到 Game Chat 2 后,音频流就不能用于预编码音频操作。

当关闭的 pre_encode_audio_stream 返回其所有资源时,该流已销毁并且将通过 game_chat_stream_state_change 结构来通知应用,其中该结构的 state_change_type 字段设置为 game_chat_stream_state_change_type::pre_encode_audio_stream_destroyed。 应该清除对此流的任何引用或指向它的指针。 在此流状态更改返回到 Game Chat 2 后,音频流内存无效。

流用户

与流相关联的用户列表,可通过使用 pre_encode_audio_stream::get_users() 进行检查。

音频格式

缓冲区音频格式,应用从 Game Chat 2 检索而来,可使用 pre_encode_audio_stream::get_pre_processed_format() 进行检查。 预处理的音频格式为单声道。 应用应该会处理以 32 位浮点数、16 位整数和 32 位整数表示的数据。

通过使用 pre_encode_audio_stream::set_processed_format(),应用必须通知 Game Chat 2 将提交给操作缓冲区的音频格式,以用于编码和传输。 用于预处理音频流的已处理格式必须满足以下先决条件。

  • 格式必须为单声道。
  • 格式必须为 32 位浮点性能监视计数器 (PCM)、32 位整数 PCM 或16 位整数 PCM 格式。
  • 根据其平台,格式的采样率必须满足相应的先决条件。 Xbox One ERA 和 Xbox Series X|S 支持 8 KHz、12 KHz、16 KHz 和 24 KHz 采样率。 适用于 Xbox One 和 Windows 电脑的通用 Windows 平台 (UWP) 支持 8 KHz、12 KHz、16 KHz、24 KHz、32 KHz、44.1 KHz 和 48 KHz 采样速率。

检索和提交预编码音频

应用可使用 pre_encode_audio_stream::get_available_buffer_count() 查询预编码音频流,以获得要处理的可用缓冲区数。 如果应用想要延迟音频处理,直到可用缓冲区最少时,则可以使用此信息。

每个预编码音频流将仅有 10 个缓冲区排队,音频延迟会导致音频管道出现延迟。 建议应用在排队超过 4 个缓冲区前先排出其预编码的音频流。

使用 get_next_buffer() 检索音频缓冲区

应用可使用 pre_encode_audio_stream::get_next_buffer(),从预编码音频流检索音频缓冲区。 新的音频缓冲区平均每 40 ms 提供一次。

此方法返回的缓冲区用完后,必须释放到 pre_encode_audio_stream::return_buffer()

对于预编码音频流,任何给定时间最多可存在 10 个排队的或未返回的缓冲区。 达到此限制后,将丢弃从用户音频源中捕获的新缓冲区,直到返回某些未完成的缓冲区。

使用 submit_buffer() 提交音频缓冲区

应用可使用 pre_encode_audio_stream::submit_buffer() 将已检查和操作的音频缓冲区提交回 Game Chat 2,以便进行编码和传输。 游戏聊天 2 支持内置和外置的音频操作。 提交到 pre_encode_audio_stream::submit_buffer() 的缓冲区不必与从 pre_encode_audio_stream::get_next_buffer() 检索到的缓冲区相同。

基于与该流相关联的用户,将为这些提交的缓冲区应用隐私/权限。 每隔 40 ms 将对此流中下一个 40 ms 的音频进行编码和传输。

为防止音频暂时中断,应以恒定速率将应持续听到的音频的缓冲区提交到此流。

流上下文

应用可使用 pre_encode_audio_stream::set_custom_stream_context()pre_encode_audio_stream::custom_stream_context() 来管理预编码音频流上的自定义指针大小的上下文值。 这些自定义流上下文对于在游戏聊天 2 的音频流和辅助数据之间创建映射非常有用。 例如,流的元数据和游戏状态。

示例

下面是一种简化的端到端示例,可用于在一个音频处理帧中使用预编码音频流。

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);

操作后期解码的聊天音频数据

游戏聊天 2 通过使用 post_decode_audio_source_streampost_decode_audio_sink_stream 类别,提供对后期解码聊天音频数据的访问。 这意味着用户可以具体针对聊天音频的每个本地接受器对远程用户进行音频操作。

源和接收器

与预编码管道不同,用于处理后期解码音频数据的模型分为两个类:post_decode_audio_source_streampost_decode_audio_sink_stream

可从 post_decode_audio_source_stream 对象检索来自远程用户的已解码音频,进行操作并发送到 post_decode_audio_sink_stream 对象用于呈现。 这样游戏聊天 2 的后期解码音频处理管道和有用的音频中间件之间就可以进行集成。

流生命周期

当新的 pre_encode_audio_streampost_decode_audio_sink_stream 实例准备就绪供应用使用时,它将通过 game_chat_stream_state_change 结构来传递,其中该结构的 state_change_type 字段分别设置为 game_chat_stream_state_change_type::post_decode_audio_source_stream_createdgame_chat_stream_state_change_type::post_decode_audio_sink_stream_created。 在此流状态更改返回到 Game Chat 2 后,音频流就可以用于后期解码音频操作。

现有 post_decode_audio_source_streampost_decode_audio_sink_stream 不可用于音频操作时,将通过 game_chat_stream_state_change 结构来通知应用,其中该结构的 state_change_type 字段分别设置为 game_chat_stream_state_change_type::post_decode_audio_source_stream_closedgame_chat_stream_state_change_type::post_decode_audio_sink_stream。 这时应用有机会开始清理与此音频流相关联的资源。 在此流状态更改返回到 Game Chat 2 后,音频流就不能用于后期解码音频操作。 对于源流而言,这意味着不会有更多的缓冲区排队等候操作。 对于接收器流而言,这意味着已提交的缓冲区不再呈现。

当关闭的 pre_encode_audio_streampost_decode_audio_sink_stream 返回其所有资源时,该流已销毁并且将通过 game_chat_stream_state_change 结构来通知应用,其中该结构的 state_change_type 字段分别设置为 game_chat_stream_state_change_type::post_decode_audio_source_stream_destroyedgame_chat_stream_state_change_type::post_decode_audio_sink_stream_destroyed。 应该清除对此流的任何引用或指针。 在此流状态更改返回到 Game Chat 2 后,音频流内存无效。

流用户

与后期解码源流相关联的远程用户列表,可使用 post_decode_audio_source_stream::get_users() 进行检查。

与后期解码接收器流相关联的本地用户列表,可使用 post_decode_audio_sink_stream::get_users() 进行检查。

音频格式

缓冲区音频格式,应用从 Game Chat 2 检索而来,可使用 pre_encode_audio_stream::get_pre_processed_format() 进行检查。 预处理音频格式始终是单声道,16 位整数 PCM。

通过使用 pre_encode_audio_stream::set_processed_format(),应用必须通知 Game Chat 2 将提交给操作缓冲区的音频格式,以用于编码和传输。 用于后期解码音频接收器流的已处理格式必须满足以下先决条件。

  • 格式必须拥有不超过 64 个通道。
  • 格式必须是 16 位整数 PCM(最佳)、20 位整数 PCM(在 24 位容器中)、24 位整数 PCM、32 位整数 PCM 或 32 位浮点 PCM(16 位整数 PCM 之后的首选格式)。
  • 格式的采样率必须介于每秒 1000 个样本和 200000 个样本之间。

检索和提交后期编码音频

应用可使用 pre_encode_audio_stream::get_available_buffer_count() 查询后期解码音频源流,以获得要处理的可用缓冲区数。 如果应用想要延迟音频处理,直到可用缓冲区最少时,则可以使用此信息。 每个后期解码音频源流将仅有 10 个缓冲区排队,音频延迟会导致音频管道出现延迟。 建议应用在排队超过 4 个缓冲区前先排出其后期解码的音频流。

使用 get_next_buffer() 检索音频缓冲区

应用可使用 pre_encode_audio_stream::get_next_buffer() 从后期解码音频源流检索音频缓冲区。 新的音频缓冲区平均每 40 ms 提供一次。

此方法返回的缓冲区用完后,必须释放到 post_decode_audio_source_stream::return_buffer()

对于后期解码音频源流,任何给定时间最多可存在 10 个排队的或未返回的缓冲区。 达到此限制后,将丢弃来自远程用户的新解码缓冲区,直到返回某些未完成的缓冲区。

使用 submit_buffer() 提交音频缓冲区

应用可使用 post_decode_audio_sink_stream::submit_mixed_buffer() 将已检查和操作的缓冲区提交回 Game Chat 2,以便通过后期解码音频接收器流进行呈现。 游戏聊天 2 支持内置和外置的音频操作。 提交到 post_decode_audio_sink_stream::submit_mixed_buffer() 的缓冲区不必与从 post_decode_audio_source_stream::get_next_buffer() 检索到的缓冲区相同。

每隔 40 ms 将对此流中下一个 40 ms 的音频进行呈现。 为防止音频暂时中断,应以恒定速率将应持续听到的音频的缓冲区提交到此流。

隐私和混合

由于后期解码管道的源接收器模型,应用有责任将从 post_decode_audio_source_stream 对象检索到的缓冲区混合,然后向 post_decode_audio_sink_stream 对象提交此混合缓冲区来进行呈现。 这也意味着应用有责任在执行该混合时保证适当的隐私和特权。 Game Chat 2 提供了 post_decode_audio_sink_stream::can_receive_audio_from_source_stream(),以便简单高效地查询此信息。

聊天指示器

后期解码音频操作不会影响每个用户的聊天指示器状态。 例如,如果远程用户已静音,则音频将提供给应用。 但该远程用户的聊天指示器仍指示静音。

当远程用户正在说话时,其音频已提供。 但是无论应用是否提供了包含该用户音频的音频混音,聊天指示器均指示说话。 有关 UI 和聊天指示器的详细信息,请参阅使用游戏聊天 2

如果使用特定于应用的额外限制来确定音频混音中出现了哪些用户,则应用有责任在读取由游戏聊天 2 提供的聊天指示器时将这些相同的限制考虑在内。

流上下文

应用可使用 post_decode_audio_source_stream::set_custom_stream_contexpost_decode_audio_source_stream::custom_stream_context 方法来管理后期解码音频流上的自定义指针大小的上下文值。 这些自定义流上下文对于在游戏聊天 2 的音频流和辅助数据之间创建映射非常有用。 例如,流的元数据和游戏状态。

示例

下面是一种简化的端到端示例,可用于在一个音频处理帧中使用后期解码音频流。

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);

聊天用户生命周期

启用实时音频操作会影响聊天用户的生命周期。 如果调用了 chat_manager::remove_user(chatUserX),由 chatUserX 指向的 chat_user 对象将保持有效,直至引用 chatUserX 的所有音频流均已销毁。

请考虑以下方案。

// 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()

另请参阅

游戏聊天 2 简介

使用游戏聊天 2 C++ API

API 内容 (GameChat2)

Microsoft 游戏开发工具包