在 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;

因此,提示音播放完成不只是“这一段声音播完了”,还包括原来的音乐、音量和输出状态都回到正确状态。理解这一点,也就抓住了提示音音频链的最后一环。


原文:CSDN · TWS 耳机提示音是怎样播放出来的