这篇梳理音乐音量在应用层的处理:手机下发的音量怎样换算为本地等级,左右耳怎样同时生效,以及耳机按键调节后怎样同步手机音量条。通话音量只作区分,不展开 HFP 细节。

一、手机调节音量后的主流程

一次音量调节要对应下面四处状态:

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart LR
    N0["手机音量条"]
    N1["耳机内部音量级"]
    N2["左右耳同步状态"]
    N3["DAC 实际输出音量"]
    N0 --> N1
    N1 --> N2
    N2 --> N3
    class N0,N1,N2,N3 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;

手机和耳机的音量范围并不相同。AVRCP 绝对音量是 0~127,SDK 内部可配置为 16 级或 32 级,最后还要通过音量表转换为 DAC 使用的数字音量。

因此,手机显示 50% 不等于把数值 50 直接写入 DAC,而是先换算、再查表。

这里的“绝对音量”指手机音量条和耳机媒体音量相互对应:任一侧调节,另一侧都要更新。

手机调节音乐音量后,协议栈将新的媒体音量交给应用层,主线如下:

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
    subgraph R0["① 接收与换算"]
        direction LR
        R0N0["收到手机设置的媒体音量"]
        R0N1["把 0~127<br/>换算为本地音量级"]
        R0N2["发起 TWS 定时音量请求"]
        R0N0 --> R0N1 --> R0N2
    end
    class R0N0,R0N1,R0N2 blue;
    style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
    subgraph R1["② 定时执行与保存"]
        direction LR
        R1N0["左右耳在约定时间设置 DAC"]
        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;

TWS 路径不会在当前回调里直接设置 DAC,而是先发起定时请求;到达定时点后,左右耳再一起设置实际音量。

非双耳场景只设置本机;双耳连接时,需要两边使用同一个目标值,并在同一时刻生效。

二、为什么只同步音量值还不够

主耳和从耳最终即使都是音量 10,只要一侧早几十毫秒生效,听感仍会短暂偏向另一侧。双耳同步既要同步目标值,也要同步执行时间。

%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart LR
    N0["主耳收到目标音量"]
    N1["把目标和定时信息<br/>交给 TWS"]
    N2["左右耳等待<br/>同一个执行点"]
    N3["同时更新实际输出音量"]
    N0 --> N1
    N1 --> N2
    N2 --> N3
    class N0,N1,N2,N3 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;

手机上滑动调节时,两边的起点、目标值和步进也要一致,否则中间过程仍可能一边先变大或先静音。

排查不同步时,除了最终音量值,还要对比两边收到目标值、进入定时回调和写入 DAC 的时间。

三、耳机按键调节为什么还要通知手机

%%{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["启用绝对音量时<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;

耳机只改本地音量而不通知手机时,手机仍会显示旧音量;下一次下发也可能覆盖耳机当前值。

音乐主要使用 A2DP/AVRCP 音量,通话使用独立的 HFP 音量。SCO 建立后切到通话音量,结束后恢复音乐音量;排查通话前后突变时,先确认当前取的是哪一份值。

四、出现异常时怎么顺着链路排查

1. 手机音量条变化,耳机声音没有变化

依次确认:手机音量事件是否到达应用层、协议值是否换算正确、定时回调是否执行、DAC 音量是否写入。

2. 左右耳声音短暂不一致

先比较两边的目标值,再比较进入定时回调和设置 DAC 的时间,不要只看最终保存的音量。

3. 耳机按键已经调节,手机音量条没有变化

先确认是否启用绝对音量,再看本地变化后有没有走到手机通知链路。声音变化只说明本地设置成功,不代表手机已同步。

4. 快速滑动时声音突然跳变

记录手机下发和耳机实际执行的音量序列,先看中间值是否丢失。请求过密时可保留最新目标值并平滑过渡,但左右耳的平滑状态也要一致。


原文:CSDN · 【中科蓝讯】TWS音量是怎样同步的?