使用实时音频操作以应用自定义语音效果

PlayFab Party 是一种实时网络和语音聊天解决方案。 在为语音聊天进行配置时,PlayFab Party 会传输麦克风音频,并在其未修改的情况下进行回放。 一些游戏需要访问语音聊天音频缓冲区以实现自定义音频效果,例如空间音频或语音筛选器。 本文档将提供操作实例,介绍如何使用实时音频操作功能以在 PlayFab Party 中截获并修改语音聊天音频。

先决条件

本操作实例假设你基本熟悉 PlayFab Party 中的语音聊天

平台支持

实时音频操作并非在所有平台上都可用。 虽然与实时音频操作关联的方法存在于统一的跨平台标头中,但它们当前仅适用于 Windows、Xbox 和 PlayStation® 5。 在其他平台上,这些方法将返回错误。

音频流

实时音频操作引入了音频流的概念,用于从库中检索音频或将音频提交到库。 音频流的类型有两种。 第一种是 源流。 源流用于从聊天控件中检索音频。 每个聊天控件只能有一个源流,称为 语音流。 对于本地聊天控件,此用于检索麦克风输入;对于远程聊天控件,此用于检索传入的语音音频。 如果聊天控件存在语音流,则库会将该聊天控件的源音频重定向到其语音流,而不是自动处理音频。 对于本地聊天控件,这意味着将麦克风音频重定向到语音流,而不是对其进行自动编码和传输;对于远程聊天控件,这意味着将传入的语音音频重定向到语音流,而不是自动将其提交到每个要回放的本地聊天控件。 源流由 PartyAudioManipulationSourceStream 表示。

第二种类型的流是 接收器流。 接收器流用于将音频提交到聊天控件。 只有本地聊天控件可以有接收器流,且每个控件可以有两个接收器流。 它们被称为 捕获流呈现流。 如果聊天控件存在捕获流,则库将从捕获流中拉取音频进行编码并将其传输到其他聊天控件而非麦克风。 如果聊天控件存在呈现流,除了 从远程聊天控件自动回放的语音聊天音频外,库还将从呈现流中拉取音频并进行回放。 提交到捕获流的音频用作本地聊天控件的麦克风输入;提交到呈现流的音频将回放或“呈现”到本地聊天控件的音频输出设备。 接收器流由 PartyAudioManipulationSinkStream 表示。

正在配置音频流

默认情况下,库会处理音频检索、传输以及回放。 因此,聊天控件在无音频流的情况下创建。 可以通过流配置方法 - PartyLocalChatControl::ConfigureAudioManipulationCaptureStream()PartyLocalChatControl::ConfigureAudioManipulationRenderStream() 以及 PartyChatControl::ConfigureAudioManipulationVoiceStream() 为聊天控件创建一个或多个流。 配置完成后,随后便可通过 PartyLocalChatControl::GetAudioManipulationCaptureStream()PartyLocalChatControl::GetAudioManipulationRenderStream() 以及 PartyChatControl::GetAudioManipulationVoiceStream() 对流进行检索

每种流配置方法允许指定将从流中检索或提交到流的音频的格式。 有关支持的格式的详细信息,请参阅每种流配置方法的参考文档。

从源流中检索音频

可以通过 PartyAudioManipulationSourceStream::GetNextBuffer() 从源流中检索音频。 当检测到语音活动时,约每 40 毫秒将有一个新缓冲区可用。 如果没有可用的缓冲区,则调用将成功并提供零长度缓冲区。 可以通过 PartyAudioManipulationSourceStream::GetAvailableBufferCount() 检索即时可用的缓冲区总数。

为了提高效率,GetNextBuffer() 提供了指向库内存的缓冲区,而不是复制整个缓冲区。 可以选择性对其进行适当修改。 处理完缓冲区后,应通过 PartyAudioManipulationSourceStream::ReturnBuffer() 将其释放,以便库可以回收其内存。 在返回缓冲区之前可以检索多个缓冲区,且不需要按照检索顺序返回缓冲区。

将音频提交到接收器流

可以通过 PartyAudioManipulationSinkStream::SubmitBuffer() 将音频提交到接收器流。 缓冲区由库复制,可以在调用完成后立即释放。

每 40 毫秒,库会消耗已提交到接收器流的 40 毫秒音频。 要防止音频中断,应按恒定速率提交音频。

场景

麦克风音频操作(即预编码缓冲区操作)

麦克风音频操作是在将麦克风音频传输到其他聊天控件之前对其进行拦截和更改的行为。 这有时被称为“预编码缓冲区操作”,因为在对麦克风音频进行编码并将其传输到其他聊天控件之前会对其进行修改。 如果你希望为本地聊天控件实现此场景,请先为本地聊天控件配置语音流和捕获流。 配置后,由专用音频线程的每个时钟周期调用、用于处理该单个聊天控件的麦克风音频的函数可能如下所示。

// An app-defined function that takes a microphone buffer and generates a new
// buffer that should be transmitted to other chat controls.
std::vector<uint8_t>
ProcessLocalVoiceBuffer(
    PartyMutableDataBuffer* inputBuffer
    );

void
ProcessLocalMicrophoneAudioForSingleChatControl(
    PartyLocalChatControl* chatControl
    )
{
    // Get the voice stream from which we want to retrieve audio. This provides
    // the audio generated by the chat control's input device.
    PartyAudioManipulationSourceStream* voiceStream;
    RETURN_VOID_IF_FAILED(chatControl->GetAudioManipulationVoiceStream(&voiceStream));

    // Get the capture stream to which we want to submit audio. This is used to
    // submit audio that will be transmitted to other chat controls.
    PartyAudioManipulationSinkStream* captureStream;
    RETURN_VOID_IF_FAILED(chatControl->GetAudioManipulationCaptureStream(&captureStream));

    // Get the next audio buffer from the voice stream.
    PartyMutableDataBuffer buffer;
    RETURN_VOID_IF_FAILED(voiceStream->GetNextBuffer(&buffer));

    // If we retrieved a buffer, process it.
    if (buffer.bufferByteCount > 0)
    {
        // Use the buffer we retrieved to generate a new buffer that will be
        // treated as the "real" capture input and transmitted to other chat
        // controls.
        std::vector<uint8_t> processedBuffer = ProcessLocalVoiceBuffer(&buffer);

        // Convert the buffer to a Party type.
        PartyDataBuffer partyBuffer;
        partyBuffer.bufferByteCount = static_cast<uint32_t>(processedBuffer.size());
        partyBuffer.buffer = processedBuffer.data();

        // Submit the processed buffer to the capture stream.
        PartyError error = captureStream->SubmitBuffer(&partyBuffer);
        if (PARTY_FAILED(error))
        {
            printf("Failed to submit buffer to sink stream! error = 0x%08x", error);
        }

        // Return the original buffer back to the voice stream.
        error = voiceStream->ReturnBuffer(buffer.buffer);
        if (PARTY_FAILED(error))
        {
            printf("Failed to return buffer to source stream! error = 0x%08x", error);
        }
    }
}

远程音频操作(即解码后缓冲区操作)

远程音频操作是在将传入音频呈现到每个本地聊天控件之前对其进行拦截和更改的行为。 这有时被称为“解码后缓冲区操作”,因为在解码后,传入音频在呈现之前会进行修改。 如果你希望实现此场景,请先为每个远程聊天控件配置语音流,并为每个本地聊天控件配置呈现流。 然后,音频线程的每个时钟周期应从每个语音流中拉取音频、将音频混合到单个流中,同时选择性地应用效果,并将混合的缓冲区提交到每个呈现流。 根据游戏场景,你可能需要将缓冲区混合到每个本地聊天控件的不同流中。 由专用音频线程的每个时钟周期调用、用于处理传入语音音频的函数可能如下所示。

// This is an app-defined function that takes a local chat control and list of remote voice buffers and generates
// a single mixed buffer to submit to the local chat control's audio output.
std::vector<uint8_t>
GetOutputMixBuffer(
    PartyLocalChatControl& localChatControl,
    const std::map<PartyAudioManipulationSourceStream*, PartyMutableDataBuffer>& remoteVoiceBuffers
    );

void
ProcessRemoteVoiceAudio(
    const std::vector<PartyChatControl*>& remoteChatControls,
    const std::vector<PartyLocalChatControl*>& localChatControls
    )
{
    std::map<PartyAudioManipulationSourceStream*, PartyMutableDataBuffer> remoteVoiceBuffers;

    // Acquire voice buffers from each remote chat control.
    for (auto remoteChatControl : remoteChatControls)
    {
        // Get the voice stream for this chat control from which we will retrieve audio.
        PartyAudioManipulationSourceStream* voiceStream;
        PartyError error = remoteChatControl->GetAudioManipulationVoiceStream(&voiceStream);
        if (PARTY_FAILED(error))
        {
            printf("Failed to get voice stream! error = 0x%08x", error);
            continue;
        }

        // Get the next audio buffer from the voice stream.
        PartyMutableDataBuffer buffer;
        error = voiceStream->GetNextBuffer(&buffer);
        if (PARTY_FAILED(error))
        {
            printf("Failed to get next buffer! error = 0x%08x", error);
            continue;
        }

        // If we retrieved a buffer, cache it in the map for mixing.
        if (buffer.bufferByteCount > 0)
        {
            remoteVoiceBuffers[voiceStream] = buffer;
        }
    }

    // If we didn't acquire any source buffers, we don't have anything to mix.
    if (remoteVoiceBuffers.empty())
    {
        return;
    }

    // Mix the voice buffers and submit to each render stream.
    for (auto localChatControl : localChatControls)
    {
        // Get the render stream for this chat control to which we will submit audio.
        PartyAudioManipulationSinkStream* renderStream;
        PartyError error = localChatControl->GetAudioManipulationRenderStream(&renderStream);
        if (PARTY_FAILED(error))
        {
            printf("Failed to get render stream! error = 0x%08x", error);
            continue;
        }

        // Mix the buffers the buffers to generate a new, mixed buffer.
        std::vector<uint8_t> mixedBuffer = GetOutputMixBuffer(*localChatControl, remoteVoiceBuffers);

        // Convert the buffer to a party type.
        PartyDataBuffer partyBuffer;
        partyBuffer.bufferByteCount = static_cast<uint32_t>(mixedBuffer.size());
        partyBuffer.buffer = mixedBuffer.data();

        // Submit the mixed buffer to the render stream.
        error = renderStream->SubmitBuffer(&partyBuffer);
        if (PARTY_FAILED(error))
        {
            printf("Failed to submit buffer to render stream! error = 0x%08x", error);
        }
    }

    // Release the voice buffers.
    for (auto voiceBuffer : remoteVoiceBuffers)
    {
        // Return the voice buffer that we had cached from this voice stream.
        PartyError error = voiceBuffer.first->ReturnBuffer(voiceBuffer.second.buffer);
        if (PARTY_FAILED(error))
        {
            printf("Failed to return buffer! error = 0x%08x", error);
        }
    }
}

隐私和混合注意事项

只要远程聊天控件生成音频,且 聊天权限静音 配置允许至少一个本地聊天控件回放音频,则库将通过远程聊天控件的语音流提供音频。 如果应为某个而非另一个本地聊天控件播放音频,则必须将其从音频混合中省略,以供后一个聊天控件使用。

聊天指示器注意事项

为远程聊天控件配置语音流将不会影响其 聊天指示器。 你可能需要实现逻辑以协调聊天指示器和混合逻辑之间的差异,从而选择正确的 UI 指示器。 例如,聊天指示器可能指示聊天控件正在说话,但自定义混合逻辑可能选择删除音频。

混合场景

在一些场景中,你可能希望为某些而非其他聊天控件启用音频操作。 例如,你可能希望将效果应用于在游戏比赛期间遇到的对手,而非同一团队中的玩家。 在这种场景中,你可以遵循之前为远程音频操作概述的步骤,同时只为要应用音频效果的聊天控件配置语音流。 只要静音和权限配置允许,其余远程聊天控件的音频将自动呈现到本地聊天控件。