WebSocket 二进制音频链路重构:让 ESP32 AI 玩偶从“能对话”走向“连续对话”
先说结论:做 AI 硬件玩具,最坑的不是大模型接口调不通,而是“能对话”这三个字背后的链路设计。去年我接手了一个 ESP32 AI 玩偶项目,最初交付的版本是“问一句、答一句”:录音、上传、等待、播放,循环往复。用户每次说话都要等 3 到 5 秒的静默期,完全没有“对话感”。后来我们花了三周时间重构音频链路,把原本基于 HTTP 的短连接改成 WebSocket 长连接,把文本协议换成了二进制音频帧,最终让整只玩偶从“一问一答”进化到“边说边听、能随时打断”,体验完全上了一个台阶。
这篇文章把这次重构中踩过的坑、拆过的流程、定过的帧格式和调试经验完整记录下来。适合手里正好在调 ESP32 语音设备、或者准备做类似 AI 硬件的朋友参考。里面会有一些代码段和协议设计,但不会堆太多底层源码,重点是讲清楚每步为什么这么做,以及哪些地方最容易翻车。
1. 为什么从“能对话”到“连续对话”必须重构音频链路
很多开发者第一次做语音硬件,第一版都是这么干的:按钮触发录音,录完一段 WAV 或 MP3 上传到服务器,然后服务器调用语音识别和对话模型,再把回复音频返回,设备播放完,等用户下一次按键。
这版跑通很容易,但一旦真拿在手里用几天,你就会发现根本不是“对话”,而是“对讲机”加“电话留言”。尤其在 ESP32 这种资源极其有限的单片机上,问题会被放大得非常明显。
1.1 原来的架构卡在哪里
老架构的核心瓶颈主要有四个。
第一个是连接建立的成本。每次交互都要走一轮 HTTP 请求,包括 TCP 握手、HTTP 头传输、可能的 TLS 握手。ESP32 本身网络栈性能一般,Wi-Fi 环境稍有波动,三次握手加 TLS 就要耗时 200 到 500 毫秒。人耳对这个延迟极其敏感,一旦超过 500 毫秒,就会觉得“这设备反应真慢”。
第二个是头部开销。HTTP 是以文本为主的协议,哪怕只上传 100 字节的音频数据,也要背负几百字节的头。音频帧本身才多大?一帧 20ms 的 16kHz 单声道 PCM,就是 640 字节。头部已经接近音频数据的量级了,纯属浪费带宽。
第三个是上下行割裂。原有方案无法在播放回复的同时继续采集用户声音,因为没有一条稳定双向的通道。想实现“用户中途插话打断”这种功能,前端只能斗胆开多个 HTTP 请求或者不断轮询,代码写起来无比拧巴,还极不稳定。
第四个是弱网下的劣化。HTTP 短连接遇到弱网丢包,要么重传要么超时,音频流一旦断裂,用户听感就是断断续续,毫无连续可言。
其实这些问题的根源就是一句话:用短请求对话式协议去承载实时双向音频流,方向就错了。实时音频需要的是全双工、低开销、低延迟的长连接通道,WebSocket 恰好就是最贴合这个需求的协议。
1.2 WebSocket 二进制帧到底解决了什么
WebSocket 本身不是一个新协议,2011 年就是 RFC 6455 了,很多开发者早已在网页聊天、消息推送里用过。但你把它用到嵌入式音频链路上,会发现它有几个被低估的特性。
第一是长连接复用。一次握手之后,ESP32 与服务器之间保持一条 TCP 链路,后续所有音频帧都在这条链路上跑,不再重复握手,延迟大幅下降。
第二是二进制帧的低开销。WebSocket 帧头最小只有 2 字节,和 HTTP 动辄几百字节的头相比,基本可以忽略不计。这对于高频发送音频帧的场景,收益极其明显。
第三是天然的双向能力。服务器可以随时下行推送音频给 ESP32,ESP32 也可以随时上行发送用户音频。做打断、做流式播放、做多轮对话,都变成非常自然的操作。
第四是分帧边界。WebSocket 每个消息天然带有边界,服务端不用像解析 TCP 字节流那样再去处理“粘包”和“分包”。只要双方约定好帧内部格式,解析逻辑可以做得非常干净。
所以当时我评估完就决定:链路协议从 HTTP+文本格式整体切换到 WebSocket+二进制帧。后面证明这个方向完全正确,只是细节上踩了一些坑。
2. 整体方案与链路设计
这一节把重构后的整体设计思路讲明白。你会看到我为什么选择“客户端主动模式+半双工平滑切换”的方案,也会看到完整的数据流和帧格式,这部分是整个重构的地基。
2.1 链路拓扑与角色分工
整个系统由三个角色组成。
第一层是 ESP32 设备端。它负责三件事:采集麦克风数据、播放服务器下发的音频、以及运行一套轻量的状态机来管理“用户正在说话”和“设备正在播放”两个状态。
第二层是 WebSocket 网关。它运行在一台具备公网或局域网 IP 的服务器上,负责与 ESP32 保持长连接、解析二进制帧、把上行音频流转给语音识别模块,把下行 TTS 音频流转回设备。为什么叫网关而不直接叫大模型服务?因为语音识别、对话大模型、语音合成这些模块通常各自独立部署,有的还是 HTTP 接口,需要一个中间层做协议转换。
第三层是 AI 服务后端,一般拆成两段:流式语音识别端和对话+语音合成端。网关拿到用户音频后,先送流式识别,等识别文本完整后,再调用对话大模型拿到回复文本,最后调用语音合成生成音频流,一路下行回设备。
我建议把网关和大模型服务拆开,千万别把大模型 SDK 全都塞进 WebSocket 服务里。一是避免一个模型超时拖死整个连接,二是方便单独扩容。实测下来,网关进程占用非常稳定,哪怕语音识别偶尔卡顿,WebSocket 连接也不受影响,设备端不会掉线。
2.2 二进制帧格式设计
WebSocket 本身只负责消息边界,不关心业务内容。所以我们在消息体里设计了一套极简二进制帧。
每个 WebSocket 消息体用头部 4 字节标记类型和长度,后面紧跟负载数据。格式如下:
Byte 0 : 帧类型(0x01: 音频上行, 0x02: 音频下行, 0x03: 控制帧, 0x04: 心跳) Byte 1 : 编码格式标志(0x01: PCM16, 0x02: Opus) Byte 2~3 : 负载长度(大端序,uint16,最多 65535 字节) Byte 4~N : 负载数据(音频或控制指令)有人可能会问,直接用 JSON 文本包一层 base64 不就行了?开发省事,调试直观。确实省事,但代价很大:base64 会增加约 33% 的体积,JSON 解析在 ESP32 上既费内存又费 CPU,而且一旦音频数据量大,内存拷贝次数会显著增加。对微控制器来说,二进制是最友好、最可控的形式,我把这套帧结构直接写进协议文档,双方按字节对齐解析,出错的概率反而比文本低。
控制帧的负载数据我设计成简单的 key=value 文本,比如event=tts_start、event=tts_end、event=interrupt。控制这块用文本是为了调试清晰,音频部分用二进制是为了性能,这样两种需求都兼顾了。
2.3 半双工还是全双工——初期最纠结的决策
很多人一听“连续对话”,第一反应就是全双工,觉得双向音频必须实时并行。但实际上我们一开始也用全双工做过实验,结果翻车了。
问题出在设备端。ESP32 的 I2S 外设虽然有独立收发 DMA 通道,理论上可以同时录音和播放,但实际用起来,回声消除是个巨大的坑。如果设备同时开着麦克风和喇叭,麦克风会采到喇叭的声音,如果不做 AEC,远端语音识别就会把设备自己的播报识别成用户的话,整个对话立刻被自己打断。
所以在第一版重构中,我选的是半双工+快速切换方案:同一时间只有一路音频在“工作”,要么上行,要么下行,但切换速度要足够快。具体实现是:
- 设备默认处于“聆听”状态,持续上传麦克风采集的音频;
- 当服务器下发了 TTS 音频帧,设备先发送一条
tts_start控制帧,同时停掉上行采集,切换到播放模式; - 播放完毕后,再发一条
tts_end控制帧,重新回到聆听状态。
这套机制让用户几乎感觉不到切换,因为切换只发生在本地,不做网络往返,速度极快,实测从“停录”到“开播”的时间能控制在几十毫秒以内。
如果后续要做全双工,通常需要外接支持 AEC 的音频芯片,或者用像乐鑫 ESP-SR 这类带回声消除的 SDK,那就可以在软件层解决一部分问题。但以我们的资源,半双工+快速切换是这个阶段最稳、成本最低的解法。
3. 核心实现细节:从 ESP32 到服务器的完整音轨
光有协议还不够,真正让每条音频帧能跑起来,涉及一堆很琐碎但决定成败的细节。我把设备和服务器两个端的关键实现拆开讲,顺带把编解码参数的选择逻辑说清楚。
3.1 ESP32 端:I2S 采集与播放
ESP32 的音频采集我用的 I2S 外设,配合一个数字 MEMS 麦克风,比如常见的 INMP441。初始化时核心代码如下:
i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), .sample_rate = 16000, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 1024, };这里解释几个关键参数。
采样率选 16000Hz,不是拍脑袋定的。当前主流的语音识别模型,比如 Whisper 系列和许多中文 ASR 模型,原生采样率就是 16kHz,直接用这个速率可以省掉重采样这一步。16kHz 的采样率对人声识别完全够用,因为人说话的主要能量集中在 300Hz 到 3400Hz 之间。
位深选 16bit,这是绝大多数 ASR 和 TTS 引擎默认接受的格式,兼容性最好。
单声道是必然,因为单个麦克风采集,单声道省带宽,也避免双声道数据对齐的问题。
DMA 缓冲区的配置这里特别重要。dma_buf_count=8、dma_buf_len=1024的搭配,实测下来能保证在 Wi-Fi 发送偶发卡顿时也不会丢数据。如果缓冲区开太小,网络抖动一小会儿就丢帧;开太大,又会导致音频采集延迟飙升。8 组 1024 点,也就是大约 512ms 的缓冲量,既不浪费内存,又能容忍短时网络波动。
采集端还有一个细节:原本 I2S 数据到手后,需要把 32bit 容器中的 16bit 有效数据提取出来。INMP441 的数据是左对齐的,实际有效位在高 16 位,所以需要右移 16 位再转成 int16_t,然后打包上传。很多教程没提这个,直接裸传 32bit 数据,导致服务器端识别效果极差。
int16_t pcm_data[1024]; for (int i = 0; i < bytes_read / 4; i++) { int32_t raw = buffer[i]; pcm_data[i] = (int16_t)(raw >> 16); }播放端的 I2S 配置和采集类似,只是模式改成了I2S_MODE_TX,直接给 I2S 写 int16_t 数组就行。
3.2 编解码选型:为什么不直接传 PCM
音频采集上来是裸 PCM,16kHz、16bit、单声道,每秒的数据量是 32000 字节,也就是约 31.25KB/s。如果产品只在局域网环境运行,直接传 PCM 完全可行,因为不涉及跨公网带宽。
但我们的场景里,ESP32 需要通过公网连接云服务器,每秒 31.25KB 的上行带宽在很多家用宽带的弱上传链路下已经有点吃紧,遇到丢包重传,整个链路会明显变卡。所以在正式方案里,设备的音频编码用的是 Opus。
Opus 在 16kHz 采样下非常适合语音,以 16kbps 码率设置为例,每秒只产生 2KB 数据,比裸 PCM 节省了 94% 的带宽,同时语音质量依然可以接受。对 ESP32 来说,Opus 编码的开销并不算大,乐鑫的 ESP-ADF 框架里已经集成了 Opus 编解码器,PAF 平台也可以直接用。
服务器下行给设备的 TTS 音频,我直接用 16kHz 16bit 的单声道 PCM。为什么下行不压缩?因为 TTS 是服务器生成的,带宽可控,播放端如果直接拿 PCM,可以省掉解码环节。ESP32 的 CPU 主频有限,解码 Opus 虽然不是大事,但理论上能省则省,播放流程越短延迟越低。而且压缩再解压总有额外延迟,下行音频走 PCM,让链路延迟控制在最紧凑的范围。
如果你在公网环境下播放的 TTS 音频比较长,比如一段 30 秒以上的长回复,那建议下行也做 Opus 压缩,否则 ESP32 的 RAM 会被待播放音频占掉太多,可能导致系统其他任务内存不足。我们的回复通常控制在 10 秒以内,PCM 数据量约 320KB,用 ESP32 的 PSRAM 缓存完全够用。
3.3 服务器端:WebSocket 网关与 ASR/LLM/TTS 的对接
服务器端我用的 Python 的websockets库搭建网关,轻量且异步支持好。核心思路是给每个 WebSocket 连接维护一个独立的会话对象,音频帧进来之后,按序送入流式语音识别管道。
这里有个容易被忽视的问题:流式语音识别通常要求数据按时间顺序到达,但 WebSocket 消息本身不保证应用层顺序完全可靠,尤其是你的 Wi-Fi 网络出现重传时,音频帧可能乱序。所以我在网关侧给音频帧加了一个序号字段,虽然当前帧格式里没有体现,但建议在负载前增加 2 字节的 sequence number,接收端检查到乱序时直接丢弃错序帧,避免把语音特征算错。
服务器端伪代码大概是:
async def handle_audio(websocket, session): async for message in websocket: if isinstance(message, bytes): frame = parse_binary_frame(message) if frame.type == 0x01: # 上行音频 await session.asr.feed(frame.payload) elif isinstance(message, str): handle_control_message(session, message)流式识别的结果会逐步返回,识别文本稳定后,网关再把完整文本交给大模型。这里要注意,当下游大模型响应很慢时,不要让 WebSocket 连接等待太久,应该先把“收到文本”的确认帧回给设备,让设备保持低延迟感知,回复音频生成后再下行。这样可以避免用户在设备端感受到“服务器没反应”的空白期。
另外,WebSocket 网关必须做心跳保活。ESP32 在 NAT 环境下长时间不发包,路由器可能会回收映射表,导致服务器以为连接还在,但设备侧已经失联。我每 30 秒由服务器发送一个 PING 帧,设备收到后回 PONG,连续两次没回就直接断开,设备端检测到断开后主动重连。这个方法简单粗暴但非常有效,实测一整天待机不掉线。
4. 连续对话的关键优化:流式、打断与缓冲策略
链路通了只是第一步,真正让“连续”这两个字成立的,是下面三个优化点。它们直接决定了用户拿到玩偶之后的第一感觉:到底是像个玩具,还是像个“活的”对话伙伴。
4.1 从“等说完再识别”到“边说边识别”
传统按文件识别的做法是:用户说完一句话,上传整个音频文件,服务器一次性返回识别文本。这个流程在语音交互硬件上体验极差,因为用户必须等自己说完了,服务器才开始工作,延迟等于“说话时长 + 识别时间 + 大模型时间 + TTS 时间”。
重构后我接的是流式语音识别,设备边上传音频帧,服务器边识别,用户话还没说完,识别结果已经出了一部分。当识别到短语边界或静音过长的位置,网关就直接判定一句话结束,无需等待整段录音上传完毕。
这里有一个关键调节参数:静音判定阈值。太灵敏,用户中间停顿一下就被截断;太迟钝,对话节奏就慢。我最终的经验值是:当识别引擎连续 600ms 未检测到有效语音,且当前已有完整识别文本,就判定当前轮次结束。这里面 600ms 是经验值,实际需要根据你产品的目标用户微调,如果是儿童玩偶,建议放宽到 800ms,因为孩子说话停顿明显更多。
4.2 打断机制:这是“连续感”的灵魂
我做用户调研的时候,好几个人不约而同提到一个场景:玩偶在播报一个很长的故事,用户想喊停,怎么办?第一版架构完全做不到,因为播放和采集是分开的,无法打断。重构后的二进制长连接天然支持打断。
打断机制的实现逻辑并不复杂。设备保持在播放 TTS 音频时,麦克风实际上也在工作,只是音频帧不上传。我改成了播放过程中以 100ms 间隔持续监听环境音量,如果发现音量超过一个预设阈值,就立即停止播放,清空播放缓冲区,然后向服务器发送一条event=interrupt控制帧。
服务器收到打断帧后,立刻丢弃未发送完的 TTS 音频,并通知大模型停止当前生成。整个过程要求设备和服务器两头都做“快速放弃”,不能有任何“发完再停”的犹疑。实测下来,从用户开口说“停”到设备完全静音、再次进入聆听状态,控制在 300ms 以内,人耳感知就是“一说就停”。
这 300ms 的拆分大致是:音量检测 50ms,缓冲清空 100ms,控制帧上行 80ms,服务器响应 70ms。ESP32 的实时性要抓好,务必把音量检测放到中断级或高优先级任务里,不要用低优先级的轮询任务,否则会明显感觉到延迟。
4.3 网络抖动与音频缓冲策略
公网 Wi-Fi 环境下,网络抖动是不可避免的,随时可能出现 100ms 甚至 500ms 的丢包或乱序。直接播放收到的每一帧,遇到网络卡顿,用户听到的就是“卡碟”效果。
我们的做法是设备端维护一个 jitter buffer(抖动缓冲)。下行 TTS 音频到达后,先不直接播放,而是进入一个环形缓冲区,只有当缓冲区里的音频量达到 80ms 左右才开始播放。这 80ms 的缓冲会带来一点点延迟,但它换来的是当网络出现短时抖动时,播放不会立即断音,而是靠缓冲撑过去。
这个数值必须在产品测试阶段反复调。80ms 是我测下来比较均衡的折中,如果你的网络质量很好,可以降到 40ms 压缩延迟;如果是跨省公网,建议提到 120ms 更安全。
另外,服务器下行时的发送节奏也要控制。如果服务器一次性把整段 10 秒 TTS 都丢给客户端,ESP32 的 RAM 会瞬间被塞满,网络闪断时还会丢失大量数据。正确的做法是服务器以块为单位,每块 200ms 的音频发送一次,两块之间间隔 150ms 左右,形成一个“慢发快收”的节奏,让设备端缓冲区始终有数据但不满溢。这样即使某一块发丢了,设备端还有缓冲可以兜底,重传也来得及。
5. 调试这个链路时,我吃过的苦和收获的实战经验
这一节把我调试过程中遇到过的典型问题、排查思路和最终解决方案列个清单。这部分最值钱,因为全都是文档里不会写的实战细节,遇到相似症状可以直接按图索骥。
5.1 问题症状与排查过程实录
第一个头疼的问题是 ESP32 只有上行音频,服务器收到的全是噪声。一开始我以为是麦克风坏了,后来用串口把 I2S 原始 DMA 数据打印出来,发现数据非零,但全像高频噪声。排查之后才发现是 32bit 容器未做移位处理,有效 16bit 数据还在高半区,音频引擎读到低半区的填充位,全是无效数据。解决后噪声立刻消失了。
第二个问题是 WebSocket 连接总是几十秒后断开。排查时我先看服务器日志,发现并不是服务器主动断开,而是 TCP 层收到 RST。开启tcpdump抓包又发现,ESP32 在空闲时不发任何包,NAT 映射超时后连接就被静默丢弃。解决办法就是前面说的服务器每 30 秒发一次 PING 心跳,设备端必须及时回 PONG。别指望底层 TCP keepalive 单独解决,NAT 层的超时很多时候比 keepalive 时间短。
第三个问题是偶发的音频首帧延迟很大,经常听到“啵”的一声爆破音。这个出现在播放侧。I2S 刚开始播放时,DAC 输出会有一个突刺,原因往往是 DMA 缓冲区从空到非空的瞬间,内部有未初始化的数据。解决办法是播放前先向 DMA 缓冲区写几 ms 的静音数据,让 I2S 先初始化输出,然后再喂真实音频。这个细节很多参考代码不会讲,但你做到了,音质体验会立刻上一个档次。
第四个问题是当服务器在下行大音频时,上行麦克风采集会出现咔哒声。这个属于中断优先级和 CPU 调度打架。ESP32 的 I2S 收发共用中断,如果你在播放过程中频繁开关 DMA,采集侧会受到干扰。我们的解决办法是播放和采集两个 DMA 通道都保持常开,打断时不是停掉 DMA,而是停止向播放 DMA 写新数据,这样既保留了下一次采集的同步性,也避免了 I2S 重新初始化带来的咔哒声。
5.2 常见问题速查表
我把调试过程中最重要的问题浓缩成一张表,方便你开发到那一步时直接对照。
| 症状 | 可能原因 | 排查方式 | 解决建议 |
|---|---|---|---|
| 服务器端全噪声 | I2S 有效位未提取 | 串口打印原始 uint32 数据 | 右移 16 位再转 int16_t |
| WebSocket 频繁断线 | NAT 映射超时 | 抓包看 RST 来源 | 服务端 30s PING,客户端回 PONG |
| 播放时有爆破音 | DAC 未初始化直接喂数据 | 听音判断是否在首帧 | 播放前先写几句话静音 |
| 播放时采集咔哒声 | DMA 反复启停 | 观察中断频率 | 保持 DMA 常开,停止写数据即可 |
| 下行音频有撕裂感 | jitter buffer 过浅 | 播放中观察空缓冲次数 | 增加到 80~120ms |
| 打断后 TTS 还在播 | 未及时清空本地缓冲 | 打断后仍有残余音频 | 一次清空环形缓冲,再发控制帧 |
| 公网延迟高 | 未使用二进制帧 | 计算单帧开销 | 改用二进制帧降低体积 |
| 内存不够 | 下行 PCM 过大 | 查看剩余 RAM | 或改用 Opus 压缩下行音频 |
5.3 几个真正让体验起飞的小技巧
调试完稳定运行之后,我们再回头抠了一些用户体验细节。这几个小点看起来不起眼,但对“连续对话”的主观感受提升极大。
第一个是播放 TTS 前发送一个“设备端准备开始播放”的控制帧。这样即使 TTS 音频生成的延迟稍长,设备也会提前播放一段很短的提示音(比如“嗯”的一声),让整个交互节奏不突兀,用户不会觉得自己被晾在一边。
第二个是把音量检测和 WebSocket 发送任务拆分。刚开始我们直接在 WebSocket 回调里做了音量检测,结果网络一慢,整个回调任务被阻塞,导致打断反应迟钝。后来把音量检测放到单独的 FreeRTOS 任务,优先级设为中等偏上,WebSocket 发帧只管发,两件事解耦后打断响应稳定在 300ms 以内。
第三个是 ESP32 的 Wi-Fi 省电模式必须关掉,至少在连续对话期间。默认的 Wi-Fi modem sleep 会带来几十毫秒的唤醒延迟,对话场景下完全不可接受。直接用esp_wifi_set_ps(WIFI_PS_NONE)关掉省电,延迟会立刻减小。代价是功耗上升,但对于插电的桌面玩偶来说完全不是问题,如果是电池供电的产品,就需要做精细化的功耗状态切换,这里就不展开了。
6. 后续还能怎么扩展
这次重构解决了主干路径的稳定性,但“连续对话”本身还有不少可以继续深化和扩展的空间。我在项目收尾后整理出一个扩展清单,有些已经在计划内,有些则是给接手的团队留下的 roadmap。
首先是全双工 + 智能打断的完整版。当前的半双工方案能应付 95% 的对话场景,但人总有同时在听和说的需求,比如设备在放音乐时用户想直接说话。如果要做这层,建议直接上带硬件 AEC 的音频前端,或者集成乐鑫的 ESP-SR 回声消除库。注意,软件 AEC 对 CPU 占用比较高,可行的前提是你的主控是 ESP32-S3 或者 ESP32-P4 这类带较强 DSP 能力的芯片。
其次是音频链路的加密与鉴权。现在的二进制 WebSocket 链路默认跑在明文 TCP 上,局域网玩一玩没问题,但如果产品要做成联网发售的形态,建议至少要加 TLS,能有效防止不合规的中间人窃听和恶意控制帧注入。同时设备入网时要有 token 鉴权,不允许陌生设备直接连上网关。TLS 的握手会增加几十毫秒,但有硬件加速的芯片压力不大。
第三是云侧的多设备协同。目前网关只管单个设备的音频链路,如果产品系列里有多个 ESP32 设备,比如一个桌面玩偶配一个随身挂件,需要考虑同一用户的多个 WebSocket 连接如何共享会话状态。比如玩偶上问一句“我昨晚订的快递到哪了”,挂件也想知道答案,这就需要把会话信息从连接层剥离出来,放到一个独立的 session store 里,比如用 Redis。这个扩展在我下一次重构里大概率会做,到时候可以再出一篇完整的分享。
回归到核心,这次链路重构最大的收获,是我意识到做硬件语音交互产品,最不能妥协的就是链路设计的底层逻辑。短连接做交互,长连接做流,文本做控制,二进制做音频,这句话听着简单,但真正落地需要把这些原则贯穿到每一层实现里。如果你正准备做 ESP32 语音交互产品,我建议你在动手前先把这一层链路协议想清楚,不要以“先跑通”的心态开始,因为后面改起来,改的不是代码,是整个系统的骨架。