AI脑洞板
完整教案
自学课件
第 40 课 · 共 40/51 课

蓝牙对讲机 离线的两人语音

课时
待定
器材
ESP32-S3 触摸屏板 + USB 线
前置知识
无
配套代码
见项目 demo 目录

你好,我是吴老师。这份是给你自己读的——不用等我讲,看不懂就整段复制给 AI,加一句"请一次只解释一个概念,讲完问我一个小问题"。

读完之后,你应该能:

  • 说出"两块板不连 WiFi 也能连上"靠的是蓝牙 BLE GATT 的服务/特征值;
  • 明白"语音为什么要先压缩、再打包";
  • 说清"为什么必须按住才说话"(半双工)以及"掉了包也不怕"是因为每包都带状态。

用时:约 60 分钟。 准备:两块同型号触摸屏板(两人一组)都烧录 demo01_ble_walkie + 一根 USB 线。

一、让两块板子隔空说悄悄话

第 39 课板子能翻电子绘本,但那是离线的——图存在 TF 卡里,板子自己看,不用跟谁说话。这一课我们玩点更"过瘾"的:

两块板子,不连 WiFi、不插网线,能不能一台按住说话,另一台的喇叭就放出我的声音?

能。这就叫蓝牙对讲机。你已经认识蓝牙了(第 20 课):不联网,靠"广播 + 扫描"认识对方,再翻开 GATT 的"文件夹"读写。今天我们就用 BLE GATT 把语音真的传过去。

先看现象:两块板刷完全相同的程序,开机后自动配对,按住屏幕下方按钮说话,松开结束;对方喇叭真的能听到你。

二、我观察到的现象

✏️ 可以直接在表格里填写
现象勾选
两块板开机后自动配对上(不用人操作)
连上后屏幕从"正在寻找"变成"已连接,可以对讲"
按住说话,对方喇叭马上放出我的声音
松开按钮,就安静了(不能边说边听)
屏幕下方 TX / RX / LOST 三个数字一直在涨

三、声音怎么变成数字、又变回声音?

它不是"原样"飞的。这一句声音走的是一条"传送带":

环节谁在干
听声音麦克风(ES8311 录 8kHz 的声音点)
压声音IMA-ADPCM 压缩(16 位 → 4 位,声音小 4 倍)
打包每 40ms 一包(带序号 + 压缩状态)
转发BLE GATT 特征值(写 / 通知)
还原对端 ADPCM 解码 + I2S 送声卡
出声扬声器(ES8311 功放 GPIO1)

一句语音怎么到另一块板:采声 → ADPCM 压小 → 打包带状态 → BLE GATT 发送 → 解压 → 出声

四、先"压小"再寄——压缩与分包

你一开口就是"一箱苹果"(16 位原始声音太多),直接寄又大又贵。聪明点:把苹果榨成浓缩汁,只寄一小瓶;到了再兑水还原。这里"榨汁"= ADPCM 压缩,"兑水"= 对端解码。

但语音是正在流的水,不能一箱全寄,得一小口一小口地寄(每 40ms 一包)。而且无线会掉包。怎么办?就像快递箱上写"收件人 + 第几件"——每一包都带上:

  • magic:一个标记,告诉收的人"这是语音包",不是乱七八糟的数据;
  • sequence:这是第几包;
  • predictor + stepIndex:压缩时的"状态"——丢掉一包,下一包靠自带的"状态"照样能接着算,不会从头乱到尾。

⚠️ 比喻要有边界:"榨汁/还原"是指压缩能变小、能还原,不是说真把人声变成果汁。ADPCM 是为了省带宽做的小幅压缩,语音能听懂,但会丢点细节——它是有损的,不是无损、更不会让声音变好听。

五、两个代码名,够了

1. sendVoicePacket()——"把这一包语音,放进对端的'收件箱'"

我是 server(服务端):localVoiceChar->notify()      → 我"喊一声",让对端来取
我是 client(客户端):remoteVoiceChar->writeValue()  → 直接写进对端的特征值

两块板刷的是一模一样的程序,用的也是一模一样的服务 UUID 和语音特征值 UUID——就像两个同事约好"都往同一个快递柜的 3 号格子放件"。GATT 世界里,格子(特征值)有固定编号,谁放进去、谁通知对方来取,全靠这个约定。这就是为什么两块板必须刷完全相同的程序。

2. encodeAdpcmSample()——"把 16 位声音压成 4 位,还带着'状态'一起寄"

encodeAdpcmSample(sample, predictor, stepIndex)
  把 1 个 16 位声音点压成 1 个 4 位码(声音小成 1/4)

每包 40ms 有 320 个样本,压成 160 字节的 ADPCM,再装进一个 VoicePacket。这个包除了压缩语音,还带上 magic(验证是不是语音包)、sequence(第几包)、predictor + stepIndex(压缩状态)。这样即便 BLE 掉一两包,下一包自带状态还能接着算;magic 不对的包,直接丢掉,不会把乱码当语音放出来。

最该记住的:语音不是原样飞的——先被 ADPCM 压小 4 倍,再 40ms 一包地寄;而且每包都带"状态",掉一包也不慌。

先压小再寄:ADPCM 16位→4位小4倍,每 40ms 一包带 magic/序号/压缩状态,掉包也不慌

六、半双工:为什么必须按住才说话?

对讲机从来不是"你说话也能同时听对方"。这一课也一样:按住就只能发,松开才只能收。 这叫半双工。

if (按住说话 && 已连接)
    → 录音、压缩、发给对方
else if (没按住 && 收到对方的包)
    → 解码、播放

你按住时只管发;松开时才去检查有没有收到对方的包并播放。

为什么这么设计?因为扬声器就在麦克风旁边。如果一边从喇叭放对方的声音、一边还用麦克风录音,喇叭声音会灌回麦克风,变成怪怪的"嗡嗡"回声。所以"按住说话"不只是省流量,更是堵住喇叭和麦克风互相干扰(程序在按住时会暂时把喇叭"静音")。

半双工:按住只能发、松开只能收;堵住喇叭声音灌回麦克风的回声;两块板刷相同程序用相同 UUID

✏️ 可以直接在表格里填写
我观察到勾选
按住说话时,屏幕显示"正在讲话"(红),写"松开结束讲话"
松开的瞬间,屏幕变"正在接收 / 等待讲话"
我能同时听到对方说话吗?(应该不能)
我按住时,自己的喇叭是不是安静了?

七、改一个条件:不压缩 / 去掉半双工,会怎样?

① 不压缩——把 320 个 16 位样本原样装进一包(而不是压成 160 字节):

✏️ 可以直接在表格里填写
我改了什么我预测实际
不压缩,原样发 16 位一包从 167 字节涨到数百字节,超过 BLE 的 MTU(约 247 字节),装不下、更易卡、更易丢

② 去掉半双工——改成"一直发也一直听"(全双工):

✏️ 可以直接在表格里填写
我改了什么我预测实际
去掉按住,边说边听喇叭声音灌回麦克风 → 回声;双向同时流量大,更易丢包

八、一条完整的"一句对讲"链路

按住说话 → 麦克风 ES8311 录音 8kHz
     → ADPCM 压缩(16位→4位,小4倍)
     → 每 40ms 攒 320 个样本,装成一个"语音包" VoicePacket
     → 包里带:magic、序号 sequence、压缩状态 predictor/stepIndex、压缩后的语音
     → 发给对方:server 就 notify(),client 就 writeValue()
     → 对端收到 → 校验是不是真语音包 → 放进队列
     → 对端解压:4位 → 16位声音点 → 交给 I2S 送声卡 ES8311
     → 扬声器放出我的声音
     → 松开按钮 → 我这边切回"听",开播对方发来的包

真正要记住的只有一句:一句对讲 = 录音 → ADPCM 压小 → 打包带状态 → BLE GATT 特征值发出去 → 对端解压 → I2S 出声;"按住才说话"让它半双工,保住静音也省流量。

九、过关检查

  1. 两块板不连 WiFi、不连路由器,是怎么"认识"并连上的?(用上广播/扫描/服务/特征值)
  2. 为什么要先压缩(ADPCM)再发?压缩后声音小了几倍?
  3. 每一包语音里,除了压缩后的声音,还带了哪几样"说明书"?为什么带状态?
  4. 为什么必须"按住才说话"?这叫什么?
  5. 一句语音从"按住到听见",经过哪几步?
  6. 对方一直听不到,你最该先看哪两个数字?各说明什么?

留下三样东西:你和同桌对讲的那一句话、你按住说话时屏幕显示的状态、"一块板的语音怎么到另一块板"的一句话。

十、还能玩什么

作品方向描述给 AI 的效果再想想
群聊对讲3 块板连在一起,说话能被所有板子听到BLE 一般是点对点的;多条语音流怎么不打架?
加密对讲语音只有伙伴能听懂,别人听到是乱码包里的语音先"加把锁",靠什么原理?
变声对讲按住说话时加个"搞怪音效"再发改声音点(比如降调)在压缩前做还是压缩后做?
距离提示可测距离,远了才提醒靠近点用什么判断远近?(BLE 的信号强度 RSSI)

十一、蓝牙能"多人对讲"吗?——点对点 vs 广播

你可能会想:一台说话,能让好多台都听到吗? 这要分清楚两种无线的"传话方式"。

蓝牙(BLE):天生是"点对点"(一对一)

这一课做的是两块板对讲——一台说、一台听。蓝牙的 GATT 连接本质是"点对点":一块板连一块板,像打电话。

  • 一个人说、大家听? 得做成"星型":一台当中心台,其他人连它,我说话你转发给所有人。但 ESP32 做蓝牙时能同时保持的活跃连接有限(一般 3~4 个),还要自己写"转发给多个客户端",代码复杂、稳定性一般。
  • 多人同时都说话? 更难——每路语音都要占蓝牙带宽,几路挤一起会卡顿、丢包、延迟。

一句话:蓝牙更适合"一对一"的亲密对讲,做"多人"要费很大劲,还不稳定。

ESP-NOW(第 21 课):天生是"广播"(一对多)

第 21 课的 ESP-NOW 用的是广播方式:一块板按住说话 → 把语音包"广播"出去 → 射程内、同信道的所有板都收到并播放。

  • 一个人说、大家听? ✅ 天生支持:任意一块板按住 PTT,其他所有板都听到。
  • 多对多(谁都能说、谁都能听)? ✅ 支持:每块板都能按住广播,也都能收到别人的广播——这就是广播式群聊。

一句话:ESP-NOW 广播 = "任意一台说,所有人都听到",免配对、同信道即可,天生支持多人。

放到一起看

蓝牙 BLEESP-NOW 广播
基本传话点对点(打电话)广播(大喇叭)
一人说大家听要做星型+连接受限✅ 天然支持
多对多(谁都能说)难✅ 支持
免配对要配对✅ 不用配对

所以:真要"多人对讲",用 ESP-NOW 广播(第 21 课)更省事、更可靠。 本课的蓝牙对讲,适合"两块板一对一说悄悄话"的场景。

下节课预告:这一课让板子"隔空"传语音——短距离、点对点、低功耗。下一课(第 41 课)把"传"接上互联网:让板子连上 WiFi,变成一台能跟很多人一起聊天的"网络聊天室"——那要用一个所有人共用的超大"快递柜":服务器。

第 40 课 · 蓝牙对讲机

第 40 课 · 蓝牙对讲机

两块板开机自动配对,按住按钮说话、松开就发出去,对面那块板的喇叭把你说的放出来。

40BLE语音半双工