主题
第 5 章 设备端适配
跑通第 3 章后,你手里有了一台能对话的设备。本章帮你解决两个问题:
- 硬件选型:我的硬件能不能接?要配哪些声学算法?选哪种交互模式?
- 音频处理:设备端如何把麦克风的声音处理成合格的上行音频流(采集 → 前处理 → 切包 → 上行)。
读完你能做到:选定交互模式与声学算法、确定芯片型号,并把麦克风收到的声音处理成符合协议要求、能被云端 ASR 正确识别的上行音频流。
5.1 硬件能力准入基线
设备要与 JoyInside 云端稳定连接并实时上传语音,必须先具备以下 5 项底层能力。缺一项都无法接入。
| 能力 | 要求 | 一句话说明 |
|---|---|---|
| 网络接入 | 支持 Wi-Fi 或 4G | 设备能联网 |
| 全双工长连接 | 基于 WebSocket 协议 | 暂不支持 webrtc / mqtt |
| 安全加密 | HTTPS + HTTP 安全证书 | 设备端需实现证书上传与验证 |
| 音频采集 | 麦克风 + 扬声器,16k 采样率 / 16 位深 / 单声道 | 这是全书统一的音频规格 |
| 音频播放 | 能解码并播放 PCM / Opus / mp3 | 用于播放云端回复 |
⚠ 重要限制:暂不支持 webrtc / mqtt。语音对话必须走 WebSocket 长连接——这是 JoyInside 接入的唯一入口。
⚠ 芯片选型与 Demo:已支持芯片矩阵(ESP32S3 / BK7258 / Linux / 杰里), 详见 附录 C. 芯片SDK Demo。
5.2 三种交互模式选型(声学要求对照)
不同产品形态适合不同的交互模式,而每种模式对设备端声学算法的要求也不同。先用下面的决策图快速定位你该选哪种模式:
一句话选型:
- 想要最自然的对话体验(可随时打断)→ 全双工免唤醒
- 最看重隐私、或环境噪声大 → 按键对讲机
- 一问一答、指令明确即可 → 半双工 VAD
模式一:全双工免唤醒(推荐,体验最佳)
怎么交互:设备一边「播放云端语音」,一边持续「收听用户指令」,支持用户随时打断(Barge-in)。一次唤醒后无需反复呼叫唤醒词,Agent 可结合上下文进行多轮深度对话。
设备端必须具备的声学能力:
| 算法 | 要求 | 作用 |
|---|---|---|
| AEC(声学回声消除) | 必选,硬件级或算法级 | 精准过滤设备自身扬声器播放的声音,防止云端听到回音 |
| ANS(背景降噪) | 必选 | 抑制平稳噪声与非平稳突发噪声 |
| AGC(自动增益) | 必选 | 自动调节拾音音量,确保远场 / 近场信噪比达标 |
| 打断处理 | 必选 | 处理云端下发的 CALL_AGENT_INTERRUPTED 事件,停止当前播放,无缝衔接下一轮 |
适合:需要自然拟人交互的场景(陪伴玩具、音箱)。技术门槛最高,但体验最佳。
模式二:按键对讲机
怎么交互:类似「对讲机」的强控交互。用户按住实体或虚拟按键时设备开始收音,松开按键则结束收录,随后等待云端处理并播放回复。
设备端必须具备的声学能力:
| 算法 | 要求 | 作用 |
|---|---|---|
| ANS(背景降噪) | 必选 | 抑制背景环境噪音 |
| AGC(自动增益) | 必选 | 自动调节收音音量,确保声音清晰平稳 |
| 按键状态联动 | 必选 | 将按键「按下 / 松开」精准转化为音频流「开始 / 截断」信号,与云端时序同步 |
适合:强隐私场景,或环境噪声复杂的工业 / 商业设备。技术门槛最低。
模式三:半双工 VAD
怎么交互:经典的「一问一答」轮流交谈。设备播放云端语音时麦克风关闭或忽略;等播放完全结束、或通过唤醒词激活后,才开启短暂收音窗口听取用户指令。不支持播报中途打断。
设备端必须具备的声学能力:
| 算法 | 要求 | 作用 |
|---|---|---|
| ANS(背景降噪) | 必选 | 抑制背景环境噪音 |
| AGC(自动增益) | 必选 | 自动调节收音音量 |
| VAD(语音活动检测) 或 唤醒模型 | 必选 | 用静音检测判断用户何时开始说话;或唤醒模型唤醒后再收音 |
适合:信息查询、家电控制等指令明确、无需频繁探讨的场景。无需手动按键,技术门槛适中。
⚠ 端侧 VAD 结束时长必须大于 500ms,建议设置为 1000ms。
三模式速查对照
| 维度 | 全双工免唤醒 | 按键对讲机 | 半双工 VAD |
|---|---|---|---|
| AEC | 必选 | 不要求 | 不要求 |
| ANS | 必选 | 必选 | 必选 |
| AGC | 必选 | 必选 | 必选 |
| VAD / 唤醒 | 云端 VAD | 不要求 | 端侧 VAD 或唤醒模型 |
| 打断(Barge-in) | ✅ 支持 | ✅(松键即断) | ❌ 不支持 |
| 隐私 | 一般 | 最强 | 一般 |
| 技术门槛 | 高 | 低 | 中 |
| 典型场景 | 陪伴玩具 / 音箱 | 强隐私 / 工业设备 | 家电控制 / 信息查询 |
📖 算法定义详见 附录 G 术语表(VAD / AEC / AGC / ANS / OTA)。打断事件
CALL_AGENT_INTERRUPTED的处理见 4.4 WebSocket 语音通道。
5.3 设备端音频处理链路
选好交互模式(5.2)后,本节讲设备端音频从采集到上行的完整处理链路。
5.2 与 5.3 的分工:5.2 讲「需要哪些算法」,5.3 讲「这些算法在链路里怎么协同,音频怎么切包上传」。
5.3.1 音频处理链路总览
一句话概括:采集 →(AEC / ANS / AGC 前处理)→ 编码(PCM 或 Opus)→ 切包 → WebSocket 均匀上行。
5.3.2 采集与前处理(AEC / ANS / AGC 协同)
前处理是音频质量的第一道关卡,直接决定云端 ASR 能不能听清。三个算法在链路中的顺序为 AEC → ANS → AGC(见 5.3.1 总览图)。
| 算法 | 协同要点 |
|---|---|
| AEC | 消除设备自身扬声器的回声。全双工模式下扬声器的声音会被麦克风重新收到,必须用 AEC 抵消,否则云端会听到「回音」。硬件级 AEC 效果优于算法级 |
| ANS | 抑制稳态噪声(风扇、空调)与突发噪声(关门、敲击)。ANS 过度会损伤语音,需调参平衡 |
| AGC | 自动调节增益。远场说话音量小就放大,近场大就压低,保证信噪比稳定 |
关键结论:前处理在端侧完成,上行的是已处理的干净音频。云端只做 ASR 识别,不再做回声消除 / 降噪,因此端侧前处理质量直接决定识别率。
5.3.3 PCM 上行切包(硬约束)
PCM 是原始音频,切包规则简单——只有「单包帧时长」这一条硬约束。
| 规则 | 要求 |
|---|---|
| 单包帧时长 | ≤ 300ms(硬上限) |
| 单包字节数 | 时长 × 采样率 × 2bytes(16bit) × 1声道 例:16k / 16bit 下 120ms = 16000 × 2 × 0.12 = 3840 字节 |
| 发送节奏 | 间隔 = 音频时长,均匀发送(实时流式,不批量发) |
⚠ 单包超 300ms 的后果:云端 ASR 是流式处理的,单包太长会导致首字延迟增大、打断响应变慢。
建议:300ms 是硬上限,实际取 120ms 左右平衡延迟与开销。攒一批一次发(不均匀发送)会导致回复延时异常。
5.3.4 Opus 上行:单包与拼包(⭐ 建议单包上行)
Opus 是压缩格式(体积约为 PCM 的 1/8,省流量),支持 CBR / VBR。上行有两种模式:
- 单包(推荐):1 个 opus 音频包 = 1 个数据包
- 拼包(不推荐):多个 opus 音频包裸拼接成 1 个数据包
📖 三层概念(数据包 / opus 音频包 / opus 帧)与数据包构成图,见 4.5 音频格式与分片。
单包与拼包怎么选
| 模式 | 何时用 | opus 音频包格式 | frameSize |
|---|---|---|---|
| 单包(推荐) | 默认场景 | 0 号包或 3 号包均可(opus 标准都支持) | 不传 |
| 拼包(不推荐) | 仅 CPU 不足、需减少上传次数时 | 3 号包 + CBR | 传(= 单个 opus 音频包字节数) |
📖 0/1/2/3 号包、CBR / VBR 的原理见 附录 D · Opus 包结构与拼包原理。本节只讲「怎么选」,不讲「为什么」。
标准场景字节参考
参考条件:16k 采样率、8:1 压缩比、60ms 帧长 → 单个 opus 音频包约 240 字节。
(此值供拼包场景估算 frameSize 用;单包字节数随码率可变,无需关注。)
易踩坑点
audio.input.frameSize= 单个 opus 音频包字节数,仅拼包时传(单包不传);原理详见 附录 D · frameSize 语义- 设了
frameSize且小于实际 opus 音频包字节数 → 服务端按 frameSize 切错 → 解码失败 - 单个 opus 音频包字节超 480(默认上限)时,需显式设 frameSize
- 数据包总时长建议 ≤ 300ms(软建议,与 PCM 单包对齐);opus 音频包时长 ≤ 120ms 是 opus 标准硬上限
5.3.5 Mp3 下行流式音频
Mp3 是有损压缩格式(压缩比约 1:10~1:12,每分钟音频约 1MB),仅下行支持,适合下行带宽受限的场景。
启用方法:建联后,在上行 CLIENT_VOICE_CHAT_UPDATE 事件中设置 audio.output.stream = true,即可收到 Mp3 流式下行音频。
易踩坑点
| 坑点 | 说明 |
|---|---|
| 解码器复用 | 同一轮对话全程复用同一个解码器,切勿每收到一包就重新初始化,否则会丢失帧间状态、产生杂音 |
| 开头静音属正常 | Mp3 开头会有几毫秒空白/静音,属编码器/解码器延迟(Encoder/Decoder Delay),是 Mp3 算法固有特性,非数据错误,无需修复 |
📖 为什么开头会有静音:Mp3 压缩基于 MDCT(修改离散余弦变换)与多相滤波器组,采用 50% 重叠窗口消除分帧边界的加窗噪声——编码当前帧需用到前一帧后半段数据。首帧因前面无数据,编码器需人为填入一段零数据(静音)来「预热」滤波器,由此产生约 528~1160 个采样点的延迟(取决于编码器实现)。
5.3.6 上行时序与抖动控制
| 要点 | 说明 |
|---|---|
| 间隔 = 音频时长 | 实时流式发送,间隔等于单包音频时长。120ms 的包每 120ms 发一个 |
| 不批量发送 | 不能攒一批一次发,不均匀会导致回复延时异常 |
| 抖动容忍 | 网络抖动会影响 ASR 首字延迟,弱网下抖动大时首字会更慢 |
| 二进制传输 | 启用 audio.binary=true 后,上行用 ws.send(bytes)(替代 base64),省带宽 |
注意区分两件事:本节讲的是端侧实现(怎么采集、切包、发送);而建联时是否在协议层开启端侧 VAD,由
audio.vad字段控制(true开启 /false关闭,默认false)。📖 字段定义见 附录 B.3.2 配置更新 · CLIENT_VOICE_CHAT_UPDATE;协议层格式(PCM / Opus / mp3 参数、二进制配置)见 4.5 音频格式与分片。简记:§4.5 是格式字典,§5.3 是设备端落地。
5.3.7 端侧 VAD 启用全局 roundId 使用说明
功能概述
在 WebSocket 建联成功后,通过在上行音频报文的 content.mid 字段携带客户端自生成的 roundId,服务端会将其透传到 ASR、LLM 调用和 TTS 回包,实现一轮对话全链路的精准关联。
- 适用环境:
wss://ws.joyinside.com/soulmate/voiceChat/v1 - 前提条件:必须启用端侧 VAD(
"vad": true)
字段说明
| 方向 | 字段位置 | 字段名 | 生成方 | 作用 | 唯一性 |
|---|---|---|---|---|---|
| 上行 | content.mid | mid | 客户端 | 标识当前句子的所有 AUDIO 分片 | 会话内唯一 |
| 下行 | content.roundId | roundId | 服务端 | 透传客户端的 content.mid,关联整轮对话 | 与上行 content.mid 一致 |
接入步骤
第一步:启用端侧 VAD
在建连后发送 CLIENT_VOICE_CHAT_UPDATE 事件,开启端侧 VAD:
完整 JSON 示例(点击展开)
json
{
"mid": "559bce38-daa2-4bbd-b6d3-b5ff719648e5",
"contentType": "EVENT",
"uid": "1dec2354-1fc4-4ddf-a207-af31cb4c4889",
"content": {
"eventType": "CLIENT_VOICE_CHAT_UPDATE",
"eventData": {
"audio": {
"vad": true
}
}
}
}第二步:上行音频时携带 roundId
每次发送音频分片时,在 content.mid 中填入本轮对话的 roundId(建议使用 UUID 格式)。同一轮对话的所有分片使用同一个 roundId。
完整 JSON 示例(点击展开)
json
{
"mid": "1ea85016-a747-4e4f-baa2-4f756e3e2b48",
"contentType": "AUDIO",
"uid": "1dec2354-1fc4-4ddf-a207-af31cb4c4889",
"content": {
"audioBase64": "1wDVAMoArwDFAK0A...",
"index": 0,
"mid": "aceb6a01-fa2e-4ada-a8f5-9e95c3cbb4d4"
}
}第三步:通过 roundId 关联下行结果
服务端会将客户端上行的 content.mid 透传到以下下行消息的 content.roundId 字段,客户端通过该字段关联对应的上行音频。
ASR IS_FINAL 事件——mid 字段对应上行 content.mid:
完整 JSON 示例(点击展开)
json
{
"code": 200,
"msg": "ASR success",
"requestId": "a4595acb-de6c-4ac9-92b2-8fa59a7f91f7",
"mid": "aceb6a01-fa2e-4ada-a8f5-9e95c3cbb4d4",
"contentType": "ASR",
"content": {
"text": "语音测试,给我个结果",
"textType": "IS_FINAL",
"lang": "普通话"
},
"t": 1745507675867
}CALL_AGENT_START_EVENT 事件——content.roundId 对应上行 content.mid:
完整 JSON 示例(点击展开)
json
{
"contentType": "EVENT",
"content": {
"roundId": "aceb6a01-fa2e-4ada-a8f5-9e95c3cbb4d4",
"eventType": "CALL_AGENT_START_EVENT",
"eventData": {
"input": "今天天气怎么样?"
}
}
}TTS 事件——content.roundId 对应上行 content.mid:
完整 JSON 示例(点击展开)
json
{
"contentType": "TTS",
"content": {
"roundId": "aceb6a01-fa2e-4ada-a8f5-9e95c3cbb4d4",
"audioBase64": "<opus-base64>",
"audioAue": "opus",
"audioDuration": 60,
"finish": false
}
}TTS_COMPLETE 事件——content.roundId 对应上行 content.mid:
完整 JSON 示例(点击展开)
json
{
"contentType": "EVENT",
"content": {
"roundId": "aceb6a01-fa2e-4ada-a8f5-9e95c3cbb4d4",
"eventType": "TTS_COMPLETE"
}
}客户端聚合规则
收到下行 TTS 消息时,使用 content.roundId 匹配本地维护的上行 content.mid,将 TTS 音频分片归入对应的对话轮次进行播放和管理。
| 上行 | 下行 | 关联键 |
|---|---|---|
content.mid | content.roundId | 两者值相等 |
注意事项
content.mid必须在会话内唯一,建议每轮对话生成新的 UUID。- 仅在启用端侧 VAD(
"vad": true)时,该关联机制才生效。 - 顶层
mid与content.mid是不同字段:顶层mid标识报文本身,content.mid标识对话轮次。
下一步
读完本章,你已经知道硬件要配什么声学算法、音频怎么处理切包、用什么芯片。接下来:
- 跑通真实硬件:基于本章选定的芯片 Demo,在真实开发板上跑通语音对话(回 第 3 章 用真实凭证)
- 扩展业务场景:读 第 6 章 业务场景接入 —— IOT / 有声书 / 魔法打印 / 视觉对话
- 理解协议细节:4.4 WebSocket 语音通道 打断事件、4.5 音频格式与分片、附录 B.3.2 配置更新
- 查术语与排障:附录 G 术语表(VAD / AEC / AGC / ANS)、附录 D Opus 包结构与拼包原理、第 9 章 故障排查