Skip to content

第 5 章 设备端适配 ​

跑通第 3 章后,你手里有了一台能对话的设备。本章帮你解决两个问题:

  1. 硬件选型:我的硬件能不能接?要配哪些声学算法?选哪种交互模式?
  2. 音频处理:设备端如何把麦克风的声音处理成合格的上行音频流(采集 → 前处理 → 切包 → 上行)。

读完你能做到:选定交互模式与声学算法、确定芯片型号,并把麦克风收到的声音处理成符合协议要求、能被云端 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.midmid客户端标识当前句子的所有 AUDIO 分片会话内唯一
下行content.roundIdroundId服务端透传客户端的 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.midcontent.roundId两者值相等

注意事项 ​

  • content.mid 必须在会话内唯一,建议每轮对话生成新的 UUID。
  • 仅在启用端侧 VAD("vad": true)时,该关联机制才生效。
  • 顶层 mid 与 content.mid 是不同字段:顶层 mid 标识报文本身,content.mid 标识对话轮次。

下一步 ​

读完本章,你已经知道硬件要配什么声学算法、音频怎么处理切包、用什么芯片。接下来: