> For the complete documentation index, see [llms.txt](https://docs.convai.com/api-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/route-character-audio.md).

# 路由角色音频

控制共享房间麦克风，按身份绑定和静音每个角色的音频，并在 Unity 中测量角色播放情况。

控制麦克风采集，绑定一个 `AudioSource` 按参与者，静音或禁用某个角色的播放，并在多角色会话中读取角色的播放位置，使用 `IConvaiRoomAudioService`。当房间已连接且其成员关系开始报告参与者身份后，再使用此页面。

### 前置条件

* 一个已连接的多角色会话。请参见 [构建你的第一个多角色会话](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/quick-start.md).
* `IConvaiRoomAudioService`，通过以下方式检索 `ConvaiManager.TryGetRoomAudioService(out IConvaiRoomAudioService audioService)`，用于 `BindParticipantAudioOutput`, `SetParticipantAudioEnabled`，以及 `TryGetCharacterAudioPlayhead`。下面的静音、远程音频和 WebGL 播放成员也可通过 `ConvaiManager.Audio` 便捷门面访问。
* 每个场景 `AudioSource` 针对你希望独立收听的每个角色。

### 控制房间麦克风

麦克风作用于整个房间：一个采集设备为整个房间提供输入，并不绑定到任何单个角色。 `IsMicMuted` 报告它当前是否已静音，以及 `SetMicMuted(bool muted)` 将其静音或取消静音。订阅 `MicMuteChanged` (`Action<bool>`）即可在状态变化时作出响应，无论是你的代码还是其他内容修改了它。该门面会在其事件名前加上 `开启`，因此同一事件是 `ConvaiManager.Audio.OnMicMuteChanged`.

将麦克风静音并不会决定玩家在对谁说话。该路由由对话目标单独决定——请参见 [对话目标定位](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/conversation-targeting.md) ，以便在麦克风取消静音后更改谁接收玩家的语音。

```csharp
ConvaiManager manager = ConvaiManager.ActiveManager;
manager.Audio.SetMicMuted(true);
```

### 为每个角色绑定一个 AudioSource

调用 `BindParticipantAudioOutput(string participantIdentity, AudioSource audioSource)` 每个角色一次，使用 `ParticipantIdentity` 来自该角色的 `CharacterRoomMembership`。它返回 `false` 当 `participantIdentity` 为空，因此请检查返回值，而不要假设绑定成功。

{% code title="Assets/Scripts/MultiCharacterAudioBinder.cs" %}

```csharp
using Convai.Runtime.Room;
using UnityEngine;

public class MultiCharacterAudioBinder : MonoBehaviour
{
    [SerializeField] private AudioSource[] _characterAudioSources;

    public void BindRoomAudio(MultiCharacterRoomSession session, IConvaiRoomAudioService audioService)
    {
        for (int i = 0; i < session.Characters.Count && i < _characterAudioSources.Length; i++)
        {
            CharacterRoomMembership membership = session.Characters[i];
            if (string.IsNullOrEmpty(membership.ParticipantIdentity))
            {
                Debug.LogWarning($"[MultiCharacter] {membership.CharacterId} has no participant identity yet.");
                continue;
            }

            audioService.BindParticipantAudioOutput(membership.ParticipantIdentity, _characterAudioSources[i]);
        }
    }
}
```

{% endcode %}

{% hint style="warning" %}
当多角色会话处于活动状态时，传入的音轨仅通过成员索引匹配到某个成员关系。解析后未对应到任何成员关系的音轨不会附加到任何 `AudioSource` ——不会回退为按 `CharacterId`。按 `ParticipantIdentity`，绝不要按 `CharacterId`.
{% endhint %}

`ConvaiAudioOutput` 会将其 `AudioSource` 在每次启用时都向房间音频服务注册，而不仅仅是在其依赖项首次解析时。播放过程中被禁用再重新启用的角色会自动重新注册并保留其语音——路由并不依赖这两个组件中哪个先醒来。

调用 `SetParticipantAudioEnabled(string participantIdentity, bool enabled)` 可在不解除其绑定的情况下将已绑定参与者静音或取消静音 `AudioSource`。它返回 `false` 当没有 `AudioSource` 尚未为该身份绑定任何内容时——请先绑定它。

```csharp
// 在不解除绑定的情况下静音次要角色
audioService.SetParticipantAudioEnabled(secondaryMembership.ParticipantIdentity, false);
```

### 为房间中的其他真人路由音频

`BindParticipantAudioOutput` 和 `SetParticipantAudioEnabled` 也涵盖房间中的其他真人，例如第二位以……加入的参与者 [加入现有房间](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/join-an-existing-session.md)。真人参与者身份使用后端形式 `human:{speaker_id}`，因此通过空间 `AudioSource` 绑定来呈现另一位学习者声音的场景，会以与绑定角色完全相同的方式进行绑定。

```csharp
audioService.BindParticipantAudioOutput("human:learner-43", _otherLearnerAudioSource);
```

### 静音或禁用某个角色的播放

`SetCharacterMuted(string characterId, bool muted)` 和 `IsCharacterMuted(string characterId)` 在不更改网络接收内容的情况下控制本地播放音量——角色的音轨会继续流式传输，只有本地输出被静音。 `SetRemoteAudioEnabled(string characterId, bool enabled)` 和 `IsRemoteAudioEnabled(string characterId)` 更进一步：禁用会完全取消订阅该角色的音轨，因此根本不会接收其音频数据包。静音是音量层面的决定；禁用是带宽层面的决定。订阅 `RemoteAudioEnabledChanged` (`Action<string, bool>`，先是角色 ID，然后是新的启用状态）即可在你自己的代码或其他内容更改它时作出响应。在门面中，同一事件是 `ConvaiManager.Audio.OnRemoteAudioEnabledChanged`.

```csharp
// 在本地静音某个角色，但仍继续接收其音轨
manager.Audio.SetCharacterMuted(characterId, true);

// 完全停止接收某个角色的音轨
manager.Audio.SetRemoteAudioEnabled(characterId, false);
```

{% hint style="warning" %}
`SetCharacterMuted`, `IsCharacterMuted`, `SetRemoteAudioEnabled`, `IsRemoteAudioEnabled`，以及 `TryGetCharacterAudioPlayhead` 都以 `characterId`为键，而在一个包含两个共享同一 `CharacterId` ——见 [为什么 ID 不能共享](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/character-identity.md#why-an-id-cannot-be-shared)。它们都会解析为携带该角色 ID 的第一个成员关系，因此一旦房间中存在一个克隆体，它们都无法针对其中某个实例。请使用 `BindParticipantAudioOutput` 和 `SetParticipantAudioEnabled`，按 `ParticipantIdentity`进行键控，只要房间中可能包含克隆体——对于这种情况，它们是唯一足够精确的参与者级控制。
{% endhint %}

### 在需要用户手势的平台上启用播放

某些平台，包括 WebGL，在音频播放开始前需要用户手势。检查当前平台是否需要该手势——该属性是 `RequiresUserGestureForAudio` 在 `IConvaiRoomAudioService` 和 `RequiresUserGesture` 在 `ConvaiManager.Audio` 门面——以及 `IsAudioPlaybackActive` 以了解播放是否已在运行。调用 `EnableAudioPlayback()` 在所需手势之后，通常从绑定到点击或轻触的 UI 事件处理程序中调用。 `CanEnableAudioPlayback` 报告成功调用所需的条件当前是否已满足，因此按钮可以在满足前自行禁用。

```csharp
public void OnEnableAudioButtonClicked()
{
    if (manager.Audio.CanEnableAudioPlayback)
        manager.Audio.EnableAudioPlayback();
}
```

### 测量角色的播放位置

`TryGetCharacterAudioPlayhead(string characterId, out double playedSeconds)` 读取自当前播放信号开始以来，该角色音频实际上已渲染到输出设备的秒数。与上面的静音控件一样，它以 `characterId` 为键，并解析为第一个匹配的成员关系，因此无法将一个克隆体与另一个分开测量。在欠载期间，播放头会冻结，并会计入任何漂移校正跳过，因此它反映的是玩家实际听到的内容，而不是墙上时钟估计值。该方法在以下情况下返回 `false` 当当前平台的音频流不暴露播放头时——在这种情况下请回退到墙上时钟计时器，而不要将 `playedSeconds` 视为有效。

```csharp
if (audioService.TryGetCharacterAudioPlayhead(characterId, out double playedSeconds))
    Debug.Log($"[MultiCharacter] {characterId} has played {playedSeconds:F2}s.");
```

### 故障排除

| 症状                                                       | 原因                                                              | 修复方法                                                                                                                                |
| -------------------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `BindParticipantAudioOutput` 返回 `false`                  | `participantIdentity` 在调用执行时为空。                                 | 等待成员关系的 `CharacterStatusChanged` 或 `CharacterAdded` 事件后再绑定，然后读取 `ParticipantIdentity` 状态。                                           |
| `SetParticipantAudioEnabled` 返回 `false`                  | 没有 `AudioSource` 是否已为该参与者身份绑定。                                  | 调用 `BindParticipantAudioOutput` 先为该身份进行绑定。                                                                                          |
| 次要角色即使在绑定后也没有产生音频                                        | 绑定的身份与成员关系的实际 `ParticipantIdentity`不匹配，通常是由于按 `CharacterId` 替代。 | 重新读取 `CharacterRoomMembership.ParticipantIdentity` 来自 `session.Characters` 并重新绑定。                                                   |
| `SetCharacterMuted` 或 `SetRemoteAudioEnabled` 似乎影响了错误的实例 | 房间中存在两个成员关系共享同一个 `CharacterId`，而按角色 ID 键控的调用无法区分它们。             | 切换到 `SetParticipantAudioEnabled`，按 `ParticipantIdentity`，针对该角色。                                                                     |
| `EnableAudioPlayback` 没有效果                               | `CanEnableAudioPlayback` 为 `false` 在调用执行时。                      | 检查 `CanEnableAudioPlayback` 在调用前，并在以下情况下触发 `EnableAudioPlayback` 在支持以下内容的平台上的 UI 点击或轻触处理程序中 `RequiresUserGestureForAudio` 是 `true`. |
| `TryGetCharacterAudioPlayhead` 返回 `false`                | 当前平台的音频流不提供播放头。                                                 | 改用墙上时钟计时器跟踪经过时间。                                                                                                                    |

### 下一步

{% content-ref url="/pages/6481618ebe16f2ac14d3eecace53ed97c7f369d1" %}
[角色身份](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/character-identity.md)
{% endcontent-ref %}

{% content-ref url="/pages/f9d36c1e08a6a79b4209c545683def235a26a8e5" %}
[处理房间事件](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/handle-roster-events.md)
{% endcontent-ref %}

{% content-ref url="/pages/d8451a87588c6ea22a959d1a3ecd7ecc09422e8a" %}
[对话目标选择](/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/conversation-targeting.md)
{% endcontent-ref %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.convai.com/api-docs/zh/cha-jian-yu-ji-cheng/convai-unity-sdk/features/multi-character-sessions/route-character-audio.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
