在 TWS 耳机里,开机、蓝牙连接、低电量和降噪模式切换,通常都会伴随一段提示音。
从表面看,提示音像是准备一个音频文件,需要时调用播放函数即可。实际上,一段提示音从代码触发到左右耳真正出声,中间还要经过资源查找、播放排队、双耳同步和音频恢复。
本文以中科蓝讯 BT891x 原厂 SDK 的实现为例,重点分析提示音播放的核心流程。不同 SDK 的接口名称可能不一样,但整体思路通常可以互相参考。
一、提示音播放的完整链路
一段提示音的完整流程,可以先简化成下面五步:
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 准备播放请求"]
direction LR
R0N0["业务触发"]
R0N1["找到对应的提示音资源"]
R0N2["把播放请求加入队列"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 同步与播放"]
direction LR
R1N0["确定哪只耳机播放<br/>并完成双耳同步"]
R1N1["解码播放<br/>结束后恢复原来的音频状态"]
R1N0 --> R1N1
end
class R1N0,R1N1 green;
style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
R0 --> R1
classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;比如手机连接耳机成功以后,蓝牙业务只需要提出“播放连接成功提示音”这个请求。至于提示音文件放在哪里、左右耳什么时候开始播放、当前音乐要不要降低音量,都会继续交给提示音模块处理。
后面的内容就按这五步展开。
二、更换提示音后,为什么需要重新编译
更换提示音后,不能只把一个新的 MP3 或 WAV 文件复制到工程里。耳机固件不像电脑一样,能按文件路径临时打开音频;它只能读取已经写入 Flash 资源区的数据。
在原厂工程中,提示音素材先放在专门的资源目录。编译时,构建脚本会根据语言和功能配置,选择需要的素材,打包成固件可以读取的二进制资源,再随最终固件一起烧录。
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart LR
N0["原始提示音文件"]
N1["固件中的二进制资源"]
N2["对应资源的地址和长度"]
N0 -->|"编译时选择和打包"| N1
N1 -->|"运行时按编号查找"| N2
class N0,N1,N2 blue;
classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;这里的“重新编译”,重点是让新的提示音重新打包进固件资源区,不等同于音频格式转换。即使新素材本身已经是 SDK 支持的格式,也仍然需要经过这一步。
所以,排查“文件换了,声音却没变”时,先确认三件事:新文件有没有被构建脚本选中、重新编译后的固件有没有更新资源区、当前烧录或升级方式有没有把资源区写进去。
三、业务触发后,提示音怎样找到并排队播放
业务代码一般不直接使用提示音文件名,而是提交一个有明确含义的提示音编号,例如蓝牙连接成功、断开、低电量、左声道或降噪模式。
原厂 SDK 中,这类编号使用 RES_IDX_xxx 表示。以连接成功为例,业务提交的是:
bsp_res_play(RES_IDX_CONNECTED);
这行代码只表达两件事:要播放“连接成功”这类提示音,并且它需要按 TWS 规则播放。提示音模块再根据编号,从资源表中找到已经打包的资源;如果产品支持中英文切换,同一个编号还能对应不同语言的提示音。
找到资源后,提示音也不会马上出声,而是先进入提示音队列。这样当前提示音还没放完时,后来的请求可以等待,不会卡住蓝牙或按键业务。
队列也解释了一个常见现象:快速切换降噪和通透模式后,功能已经是最新状态,耳机却还在播报旧状态。因为旧请求可能仍排在队列里,还没有来得及播放或清理。
所以,调用播放接口只是提交请求,不代表耳机已经出声。连续操作后声音滞后或重复时,重点看旧请求是否还留在队列里。
四、双耳提示音:谁播放,什么时候播放
TWS 耳机有两只,提示音却不一定每次都要双耳播放。原厂 SDK 大致分为三种情况:
| 播放范围 | 适合的场景 |
|---|---|
| 双耳播放 | 蓝牙连接、断连、模式切换等双耳共同状态 |
| 当前耳播放 | 当前耳低电量、当前耳触控反馈 |
| 对耳播放 | 当前耳不响、由另一只耳机发声,例如指定左右声道 |
蓝牙连接属于两只耳机共享的状态,所以一般双耳播放。低电量则不同,左右耳的剩余电量可能不一样,因此由发生低电的当前耳提醒更合理。
确定了播放范围之后,双耳播放还要解决“什么时候开始”。左右耳各自收到请求后立即启动,容易因为消息传输和任务调度不同,听到一前一后的两声。原厂 SDK 的基本思路是由主耳协调:
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 双耳准备"]
direction LR
R0N0["主耳收到播放请求"]
R0N1["把同一段提示音<br/>通知给对耳"]
R0N2["左右耳提前准备好<br/>资源和播放环境"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 定时启动"]
direction LR
R1N0["约定同一个 TWS 时间点"]
R1N1["时间到达后<br/>两边一起启动"]
R1N0 --> R1N1
end
class R1N0,R1N1 green;
style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
R0 --> R1
classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;所以“双耳都有这段资源”只能保证两边可以播放,不能保证两边同时播放。响错耳朵时先检查播放范围;双耳一前一后时,再检查请求分发和同步时刻。
五、MP3 和 WAV 提示音怎样接入音频链
日常接触到的提示音素材,MP3 和 WAV 会更常见。它们都是外部音频文件,由对应的解码流程读取并输出声音。
MP3 是压缩音频。这版原厂工程中,连接、断连等语音提示多使用 MP3。WAV 可以先理解为波形音频文件,使用时要重点确认素材参数是否与当前 SDK 的解码能力匹配。具体选哪一种,还要结合固件空间、提示音长度和真机听感决定。
格式不同,后面的解码流程会有差异,但它们接入音频系统时要解决的问题是一样的:耳机可能已经在播放音乐,提示音不能只顾自己出声。
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 播放前与播放中"]
direction LR
R0N0["保存当前音频状态"]
R0N1["暂停或降低背景音乐"]
R0N2["解码并输出提示音"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 播放结束"]
direction LR
R1N0["提示音结束"]
R1N1["恢复原来的音量和音频业务"]
R1N0 --> R1N1
end
class R1N0,R1N1 green;
style R1 fill:#f0f8f3,stroke:#b9d7c4,color:#24563a
R0 --> R1
classDef blue fill:#e8f2ff,stroke:#7ba4d8,color:#23456b,stroke-width:1.5px;
classDef green fill:#e7f4ec,stroke:#78aa8b,color:#24563a,stroke-width:1.5px;
classDef amber fill:#fff3df,stroke:#d5a25b,color:#72501e,stroke-width:1.5px;因此,提示音播放完成不只是“这一段声音播完了”,还包括原来的音乐、音量和输出状态都回到正确状态。理解这一点,也就抓住了提示音音频链的最后一环。