> ## 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.

# 透過即時音訊操作套用自訂語音效果

> 使用 PlayFab Party 即時音訊操作串流攔截語音聊天緩衝區，並套用空間音訊和語音濾鏡等自訂效果。

PlayFab Party 是即時網路與語音聊天解決方案。設定用於語音聊天時，PlayFab Party 會傳輸麥克風音訊，並以未經修改的方式播放。有些遊戲需要存取語音聊天音訊緩衝區，以實作自訂音訊效果，例如空間音訊或語音濾鏡。本文件將逐步說明如何使用即時音訊操作功能，在 PlayFab Party 中攔截並修改語音聊天音訊。

## 必要條件

本逐步解說假設您對 [PlayFab Party 中的語音聊天](/zh-TW/services/playfab/community/voice-communications/concepts-chat)有基本的了解。

## 平台支援

即時音訊操作並非在所有平台上皆可使用。雖然與即時音訊操作相關的方法存在於統一的跨平台標頭中，但目前僅針對 Windows、XBOX 和 PlayStation® 5 實作。這些方法在其他平台上會傳回錯誤。

## 音訊串流

即時音訊操作引進了音訊串流的概念，用於從程式庫擷取音訊或將音訊提交至程式庫。音訊串流有兩種類型。第一種是**來源串流**。來源串流用於從聊天控制項擷取音訊。每個聊天控制項只能有一個來源串流，稱為**語音串流**。對於本機聊天控制項，這用於擷取麥克風輸入；對於遠端聊天控制項，這用於擷取傳入的語音音訊。如果聊天控制項的語音串流存在，程式庫會將該聊天控制項的來源音訊重新導向至其語音串流，而不是自動處理音訊。對於本機聊天控制項，這表示會將麥克風音訊重新導向至語音串流，而不是自動編碼並傳輸；對於遠端聊天控制項，這表示會將傳入的語音音訊重新導向至語音串流，而不是自動將其提交給每個本機聊天控制項播放。來源串流以 [`PartyAudioManipulationSourceStream`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyAudioManipulationSourceStream/partyaudiomanipulationsourcestream) 表示。

第二種串流類型是**接收串流**。接收串流用於將音訊提交至聊天控制項。只有本機聊天控制項可以擁有接收串流，而且每個本機聊天控制項可以有兩個，分別稱為**擷取串流**和**轉譯串流**。如果聊天控制項的擷取串流存在，程式庫會從擷取串流 (而非麥克風) 提取音訊，以進行編碼並傳輸至其他聊天控制項。如果聊天控制項的轉譯串流存在，程式庫會從轉譯串流提取音訊並播放，*此外*也會播放自動從遠端聊天控制項播放的語音聊天音訊。提交至擷取串流的音訊會作為本機聊天控制項的麥克風輸入；提交至轉譯串流的音訊會播放或「轉譯」至本機聊天控制項的音訊輸出裝置。接收串流以 [`PartyAudioManipulationSinkStream`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyAudioManipulationSinkStream/partyaudiomanipulationsinkstream) 表示。

### 設定音訊串流

根據預設，程式庫會處理音訊擷取、傳輸和播放。因此，建立聊天控制項時不會有任何音訊串流。您可以透過串流設定方法為聊天控制項建立一或多個串流：[`PartyLocalChatControl::ConfigureAudioManipulationCaptureStream()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyLocalChatControl/methods/partylocalchatcontrol_configureaudiomanipulationcapturestream)、[`PartyLocalChatControl::ConfigureAudioManipulationRenderStream()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyLocalChatControl/methods/partylocalchatcontrol_configureaudiomanipulationrenderstream) 和 [`PartyChatControl::ConfigureAudioManipulationVoiceStream()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyChatControl/methods/partychatcontrol_configureaudiomanipulationvoicestream)。設定完成後，之後可透過 [`PartyLocalChatControl::GetAudioManipulationCaptureStream()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyLocalChatControl/methods/partylocalchatcontrol_getaudiomanipulationcapturestream)、[`PartyLocalChatControl::GetAudioManipulationRenderStream()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyLocalChatControl/methods/partylocalchatcontrol_getaudiomanipulationrenderstream) 和 [`PartyChatControl::GetAudioManipulationVoiceStream()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyChatControl/methods/partychatcontrol_getaudiomanipulationvoicestream) 擷取串流

每個串流設定方法都可讓您指定要從串流擷取或提交至串流的音訊格式。如需支援格式的詳細資訊，請參閱各串流設定方法的參考文件。

### 從來源串流擷取音訊

您可以透過 [`PartyAudioManipulationSourceStream::GetNextBuffer()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyAudioManipulationSourceStream/methods/partyaudiomanipulationsourcestream_getnextbuffer) 從來源串流擷取音訊。偵測到語音活動時，大約每 40 毫秒就會有一個新的緩衝區可用。如果沒有可用的緩衝區，呼叫會成功並提供長度為零的緩衝區。可透過 [`PartyAudioManipulationSourceStream::GetAvailableBufferCount()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyAudioManipulationSourceStream/methods/partyaudiomanipulationsourcestream_getavailablebuffercount) 擷取當下可用的緩衝區總數。

為了提高效率，`GetNextBuffer()` 提供的緩衝區會指向程式庫的記憶體，而不是複製整個緩衝區。您可以選擇就地修改它。處理完緩衝區後，您應該透過 [`PartyAudioManipulationSourceStream::ReturnBuffer()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyAudioManipulationSourceStream/methods/partyaudiomanipulationsourcestream_returnbuffer) 釋放它，讓程式庫可以回收其記憶體。可以在傳回任何緩衝區之前擷取多個緩衝區，而且緩衝區不需要依照擷取的順序傳回。

### 將音訊提交至接收串流

您可以透過 [`PartyAudioManipulationSinkStream::SubmitBuffer()`](/zh-TW/services/playfab/multiplayer/networking/reference/classes/PartyAudioManipulationSinkStream/methods/partyaudiomanipulationsinkstream_submitbuffer) 將音訊提交至接收串流。程式庫會複製緩衝區，因此可在呼叫完成後立即釋放。

程式庫每 40 毫秒會取用已提交至接收串流的 40 毫秒音訊。為了避免音訊中斷，應以固定速率提交音訊。

## 情境

### 麥克風音訊操作 (又稱為編碼前緩衝區操作)

麥克風音訊操作是在麥克風音訊傳輸至其他聊天控制項之前，攔截並變更該音訊的動作。這有時稱為「編碼前緩衝區操作」，因為麥克風音訊會在編碼並傳輸至其他聊天控制項之前遭到修改。如果您想要為本機聊天控制項實作此情境，請先為本機聊天控制項設定語音串流和擷取串流。設定完成後，由專用音訊執行緒的每個 tick 呼叫、用來處理該單一聊天控制項麥克風音訊的函式，看起來可能如下所示。

```cpp theme={null}
// 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);
        }
    }
}
```

### 遠端音訊操作 (又稱為解碼後緩衝區操作)

遠端音訊操作是在傳入音訊轉譯至每個本機聊天控制項之前，攔截並變更該音訊的動作。這有時稱為「解碼後緩衝區操作」，因為傳入音訊會在解碼後、轉譯前遭到修改。如果您想要實作此情境，請先為每個遠端聊天控制項設定語音串流，並為每個本機聊天控制項設定轉譯串流。接著，音訊執行緒的每個 tick 都應從每個語音串流提取音訊，將音訊混合成單一串流 (可選擇性地套用效果)，並將混合後的緩衝區提交至每個轉譯串流。依您的遊戲情境而定，您可能需要為每個本機聊天控制項將緩衝區混合成不同的串流。由專用音訊執行緒的每個 tick 呼叫、用來處理傳入語音音訊的函式，看起來可能如下所示。

```cpp theme={null}
// 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);
        }
    }
}
```

#### 隱私權與混音考量

只要遠端聊天控制項正在產生音訊，且[聊天權限](/zh-TW/services/playfab/community/voice-communications/concepts-chat-permissions-and-muting#chat-permissions)和[靜音](/zh-TW/services/playfab/community/voice-communications/concepts-chat-permissions-and-muting#muting)設定允許至少一個本機聊天控制項播放該音訊，程式庫就會透過遠端聊天控制項的語音串流提供音訊。如果音訊應為某個本機聊天控制項播放，但不應為另一個播放，您必須在後者聊天控制項的音訊混音中省略該音訊。

#### 聊天指示器考量

為遠端聊天控制項設定語音串流不會影響其[聊天指示器](/zh-TW/services/playfab/community/voice-communications/concepts-audio-troubleshooting#check-the-chat-indicators)。您可能需要實作邏輯來協調聊天指示器與混音邏輯之間的差異，以選擇正確的 UI 指示器。例如，聊天指示器可能表示聊天控制項正在說話，但自訂混音邏輯可能選擇捨棄該音訊。

### 混合情境

在某些情境中，您可能想要為部分聊天控制項啟用音訊操作，而其他則不啟用。例如，您可能想要對遊戲比賽中遇到的敵對玩家套用效果，但不對同隊玩家套用。在這類情境中，您可以依照先前針對遠端音訊操作所述的步驟，僅為要套用音訊效果的聊天控制項設定語音串流。只要靜音和權限設定允許，其餘遠端聊天控制項的音訊會自動轉譯至本機聊天控制項。


## Related topics

- [PlayFab Party 版本資訊](/zh-TW/services/playfab/multiplayer/networking/release-notes.md)
- [使用实时音频操作应用自定义语音效果](/zh-CN/services/playfab/community/voice-communications/concepts-realtime-audio-manipulation.md)
- [無障礙功能標籤](/zh-TW/build/game-principles/accessibility/accessibility-feature-tags.md)
- [XR-015 管理玩家通訊](/zh-TW/publishing/certification/xr/xr-015.md)
- [使用 GDKX 建置並執行你的第一個主機遊戲](/zh-TW/home/build-first-title/first-console-title-walkthrough.md)
