# 语音通信

> 用户流程：按下 PTT 按钮 → 发送语音 → 松开 PTT 按钮 → 停止语音
> 对应程序流程：前置条件检查 → StartCapture → 循环 TsClient.sendVoice(clientID, codec, frame) → StopCapture

---

## 一、状态树

### 语音状态

```mermaid
flowchart TD
    VOICE[语音状态<br/>维护 Opus 帧发送过程]

    VOICE --> V0[静默 Silent<br/>没有发送语音帧]
    VOICE --> V1[采集中 Capturing<br/>上层正在生成 Opus 帧]
    VOICE --> V2[发送中 Sending<br/>循环调用 sendVoice]
    VOICE --> V3[受阻 Blocked<br/>未连接、采集失败或发送异常]
```

---

## 二、时序：发送语音帧与异常停止

```mermaid
sequenceDiagram
    participant UI as PTT 控件<br/>接收按下、松开和取消操作
    participant VFSM as 语音发送状态机<br/>检查连接与频道前置条件
    participant CAP as 音频采集与编码器<br/>生成原始 Opus 帧
    participant BRIDGE as TSBridge<br/>调用 TsClient API
    participant SDK as Go SDK (gomobile)<br/>通过 SendVoice 发送语音
    participant TS as TeamSpeak 服务器<br/>转发语音给频道成员

    UI->>VFSM: PttPressed
    VFSM->>VFSM: 检查 connected 和 joinedChannel

    alt 前置条件不满足
        VFSM-->>UI: Blocked(reason)
    else 前置条件满足
        VFSM->>CAP: StartCapture()
        CAP-->>VFSM: CaptureReady
        VFSM->>VFSM: 状态改为 transmitting
        loop 每个 Opus 帧
            CAP-->>VFSM: opusFrame
            VFSM->>BRIDGE: sendVoice(clientID, codec, frame)
            BRIDGE->>SDK: gomobile SendVoice
            SDK->>TS: UDP voice frame
        end

        alt 用户松开或取消
            UI->>VFSM: PttReleased / PointerCancelled
            VFSM->>CAP: StopCapture()
            VFSM->>VFSM: 状态恢复 silent
        else 连接断开
            SDK-->>VFSM: onDisconnected(error)
            VFSM->>CAP: StopCapture()
            VFSM->>VFSM: 状态改为 blocked
        end
    end
```

---

## 三、事件依赖

### 语音前置依赖矩阵

| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
| --- | --- | --- | --- |
| `TsClient.sendVoice` | 已连接、已有可发送的 Opus 帧 | 当前用户已在有效频道 | 任一前置失效立即停止发送 |

### 权威性划分

| 数据 | 推荐权威来源 | 原因 |
| --- | --- | --- |
| 语音发送结果 | `TsClient.sendVoice` 返回值 | 表示本次本地发送调用结果，不代表对方播放成功 |

### 统一实现原则（语音相关）

- **语音只在连接和频道前置条件满足时发送。** 断开或被踢必须立即停止。
