之前我做过一个本地版的小喇叭:网页里输入一个音频地址,ESP32 收到 MQTT 命令后去拉取 MP3,然后通过 MAX98357A 播放出来。
那个版本把最基础的链路跑通了:
-
网页能发命令
-
PHP 能转 MQTT
-
ESP32 能收命令
-
ESP32 能播放 HTTP 音频
但后来我发现,只能播固定音频还不够。真正更有意思的,是让它能直接播报文本。
所以我继续往下做,把这个项目升级成了一个 TTS 播报版。
现在这版已经能做到:
-
网页输入任意文本
-
PHP 负责分段和编排
-
Python 对接火山引擎 TTS
-
MQTT 下发播放命令
-
ESP32 拉取音频流并播放
也就是说,现在不是“网页点一下播一个 MP3”,而是“网页输入一句话,设备直接开口”。
这次我做成了什么
先说最终效果。
我现在这版的主链路是:
网页输入文本 ↓PHP 编排请求 ↓Python 调用火山引擎 TTS ↓生成可播放 stream_url ↓MQTT 下发给 ESP32 ↓ESP32 拉取 HTTP MP3 流 ↓MAX98357A 播放整体上看,它已经不是一个单点功能,而是一条完整的链路:
-
前端负责输入和触发
-
PHP 负责调度
-
Python 负责生成 TTS 流
-
MQTT 负责下发控制命令
-
ESP32 负责播放
这版我现在把它定义成:
v1.0.0-mqtt
这次用到的东西
硬件还是比较简单:
-
ESP32 开发板
-
MAX98357A I2S 功放模块
-
小喇叭
-
杜邦线
软件这边主要是:
-
网页
-
PHP
-
Python
-
Mosquitto MQTT
-
火山引擎 TTS
其实真正复杂的不是某一个点,而是把这些东西串起来以后,每一层都得对上。
为什么这个版本比之前复杂很多
之前那个本地版本,逻辑相对简单:
网页输入一个 MP3 地址 ↓PHP 发布 MQTT 命令 ↓ESP32 收到命令 ↓ESP32 播放音频但 TTS 不一样。
因为这次不是提前就有一个音频文件,而是:
-
我先输入一段文本
-
服务端要先把它变成语音
-
然后设备才能去播
看起来只多了一步“TTS”,但实际上整个稳定性难度一下就上来了。
我这次踩到的几个关键坑
1. 长文本整段播,很容易中途断
一开始我当然想的是最直接的方式:
-
一整段文本
-
一次 TTS
-
ESP32 一次性拉流播完
但真做了之后很快发现,长文本比短文本难太多。
不是因为 TTS 生成不了,而是因为:
-
文本越长,播放时间越长
-
播放时间越长,ESP32 越容易撞上 WiFi 抖动
-
一旦 WiFi 抖动,HTTP 音频流就断
-
最后就会出现播放到一半停掉
所以我后来意识到:
长文本真正的问题,不是“能不能生成”,而是“单条流拉得太久,网络一抖就容易断”。
2. MQTT 控制链和 HTTP 播放链不是一回事
这个也是我中间绕了很久的一件事。
一开始如果设备没播出来,很容易下意识觉得:
-
是不是 TTS 有问题
-
是不是 Python 没返回
-
是不是页面没发出去
但后来我发现一定要把链路拆开看:
-
MQTT 是控制链
-
HTTP 是音频链
也就是说:
-
MQTT 负责告诉 ESP32 去播哪个地址
-
真正的声音,是 ESP32 自己去拉那个 HTTP 流
只要这两条链混在一起想,就很容易判断错问题到底出在哪。
3. 插播没有我一开始想的那么简单
我本来很想做一个效果:
-
第一条正在播
-
第二条来了,马上插进去
-
立刻切到下一条
但实际调试的时候发现,事情没那么顺。
ESP32 在播放 HTTP 流的时候,MQTT 并不总是稳定。也就是说,第二条命令并不一定能在播放过程中稳稳送到设备。
所以我后来没有把“强插播”当成这版的核心目标,而是先把“顺序播报稳定”做扎实。
我最后怎么收敛这个方案
既然整段长流容易断,那我后面就不再坚持“一整段一次性播完”。
最后我改成了:
1. PHP 自动断句分段
文本来了以后,先按这些标点优先断句:
-
。 -
! -
? -
; -
换行
如果某一句还是太长,再按这些继续切:
-
, -
、 -
:
最后再用长度兜底。
也就是说,我不再让 ESP32 一次扛整段长文本,而是让它一段一段播。
2. PHP 提前预生成每一段 TTS
我最开始做顺序播报时,是这样的:
播完一段 ↓再去生成下一段 TTS ↓再下发下一段这样虽然稳了,但段与段之间停顿很明显。
所以我后来改成:
先把所有分段都生成好 ↓先发第一段 ↓ESP32 播完一段 ↓立刻发下一段这样就把“播完后再临时生成”的等待时间省掉了。
虽然现在还不是完全无缝,但比之前那种“现播现做现等”顺很多。
3. ESP32 侧加本地音频缓冲
这个优化也很关键。
我最后保留的是:
-
HTTP实时流 -
ESP32本地缓冲 -
MQTT只负责控制
具体来说,就是 ESP32 在拉取 HTTP 音频流时,不是直接把每个字节立刻喂给解码器,而是先用:
AudioFileSourceBuffer
做一层本地缓冲。
我现在这版缓冲用的是:
32KB
这一步的意义很明显:
-
网络稍微抖一下,不至于立刻断
-
播放会更稳
-
长文本体验会明显好很多
我现在这版的最终结构
最后保留下来的结构就是:
用户文本 ↓PHP 自动断句分段 ↓PHP 预生成每一段 TTS stream_url ↓MQTT 下发 tts + url ↓ESP32 打开 HTTP stream ↓AudioFileSourceBuffer 本地缓冲 ↓I2S 播放 ↓MAX98357A 输出声音我现在对这套结构的判断是:
-
它已经能工作
-
它已经比最开始稳定很多
-
它不是最极致的方案
-
但它已经是一套完整跑通的链路
这版现在适合做什么
我觉得它现在比较适合这几类用途:
-
本地语音提示
-
设备播报
-
物联网语音输出
-
定时提醒
-
一些简单的文字转语音硬件联动
尤其如果你本来就在做:
-
ESP32
-
MQTT
-
I2S 音频
-
本地控制页面
那这套项目会很适合参考。
这版我还保留了哪些边界
这次我没有想把文章写成“什么都完美解决了”,因为真实情况不是这样。
1. 段与段之间还是会有空档
虽然我已经做了预生成,但本质上现在还是:
-
一段一个流
-
一段一个命令
-
一段播完再切下一段
所以它不是完全无缝。
2. WiFi 环境还是很关键
我这次调下来感受很深的一点是:
播放稳定性不只是代码问题,也很吃现场网络环境。
如果 ESP32 到路由器这一层本身有抖动,再好的逻辑也会受影响。
3. 这版不主打强插播
这版我现在更看重的是:
-
顺序播报稳定
-
长文本可用
-
整体链路清晰
而不是复杂的抢占式插播。
下一版我想继续做什么
我现在已经把这版定义成:
v1.0.0-mqtt
后面我还想继续往下迭代。
下一步我比较想试的是:
1. 继续优化传输层
当前这版还是:
-
MQTT做控制 -
HTTP做音频拉流
这套已经能工作,但播放稳定性还是比较依赖单条 HTTP 流和 WiFi 环境。
所以我后面想继续试试看:
如果部分链路改成 UDP 方向,会不会让实时性和稳定性再往前走一点。
这里我不会现在就说 UDP 一定更稳,因为它也会带来新的问题,比如:
-
丢包
-
重组
-
缓冲策略
但它确实是我下一版想认真尝试的方向。
2. 看能不能把段间停顿再压短一点
现在已经比之前顺不少了,但我还想继续往“更自然一点”的方向做。
开源地址
这个项目我已经整理成仓库了:
GitHub:
https://github.com/Kaiii-create/kai-sound-tts
当前这个版本分支:
https://github.com/Kaiii-create/kai-sound-tts/tree/v1.0.0-mqtt
演示小视频:
最后
这次我最大的感受,不是“我又做了一个功能”,而是:
一个硬件项目真正难的地方,往往不是代码本身,而是把整条链路一层一层确认清楚。
比如:
-
页面有没有把请求发出去
-
PHP 有没有真的下发 MQTT
-
Python 有没有真的生成 TTS
-
ESP32 有没有收到命令
-
ESP32 有没有真的打开流
-
WiFi 有没有中途抖动
-
I2S 和功放有没有真的在播
这些问题只要有一层没确认清楚,最后表面现象就可能完全像是另外一层出了问题。
但也正因为把这些问题一层一层拆开,我最后才把这个版本慢慢收敛出来。
对我来说,这个版本的意义不在于“它已经完美了”,而在于:
它已经不是一个想法了,而是一条真正跑通、而且已经能工作的链路。
如果你也在做类似的 ESP32 播报、语音提示、物联网语音交互项目,欢迎交流。后面我大概率还会继续往下做。