最开始看原厂 SDK 时,我觉得它的切换很简单:确定关闭、降噪或通透,再调用模式接口即可。
随着 App 控制、按键配置、单双耳记忆和出入盒切换逐渐加进来,我才发现应用层维护的并不只是一个 ANC 模式。提示音何时播放、左右耳怎样同步、状态什么时候保存和上报,也都在这条链路里。
一、先把 ANC 的几条链路放在一起
ANC 模式可能因为三类事件发生变化:
- App 下发一个明确的目标模式;
- 用户通过耳机按键切换模式;
- 耳机出入盒或 TWS 连接状态变化,需要重新选择单双耳记忆。
把三个入口放在一起,我目前是这样理解的:
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 确定目标模式"]
direction LR
R0N0["收到 ANC 相关事件<br/>App 指令、按键事件<br/>出入盒或 TWS 状态变化"]
R0N1["确定当前使用场景<br/>单耳、双耳<br/>或者双耳都在盒内"]
R0N2["得到本次目标模式<br/>关闭、通透、不同等级的降噪<br/>或自适应降噪"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 同步并应用"]
direction LR
R1N0["更新并同步状态<br/>选择或更新单耳、双耳记忆<br/>必要时通知对耳"]
R1N1["处理提示音并应用模式<br/>按触发来源处理提示音<br/>再转换成 SDK 模式"]
R1N2["保存并更新 App<br/>写入记忆;按键或状态变化时<br/>更新 App 显示"]
R1N0 --> R1N1 --> R1N2
end
class R1N0,R1N1,R1N2 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;这是一条方便理解的主线。实际代码里,App、按键和状态变化的处理顺序并不完全相同,后面再分别展开。
二、应用层同时维护了哪些 ANC 状态
结合当前代码看,我会把 ANC 状态分成四层:
| 状态 | 记录的内容 |
|---|---|
| 单耳记忆 | 单耳场景使用的模式和降噪等级 |
| 双耳记忆 | 双耳共同使用时的模式和降噪等级 |
| 当前运行状态 | ANC 硬件此时真正运行的模式 |
| App 显示状态 | 根据当前使用场景返回给 App 的模式 |
这几层状态大多数时候相同,但不能混为一个变量。以当前实现为例,恢复出厂后的双耳记忆是强降噪,单耳记忆是通透。双耳都在盒外并完成 TWS 连接时使用双耳记忆,只有一只耳机使用时恢复单耳记忆。
记忆状态也不等于实时状态。双耳都在盒内时,ANC 硬件和 App 显示为关闭,但两份记忆仍然保留,出盒后再按当前场景恢复。每份记忆还会保存降噪等级,因此从通透切回降噪时,可以恢复之前的强、中、低或自适应降噪。
三、App 下发和按键切换走的是两条路径
App 和按键最后都会改变 ANC,但处理方式不一样。
App 下发路径
App 已经给出了目标模式。耳机收到指令后,只需要判断这次修改的是单耳记忆还是双耳记忆。
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 接收与更新"]
direction LR
R0N0["App 下发目标模式和等级"]
R0N1["判断当前是单耳<br/>还是双耳场景"]
R0N2["更新对应的 ANC 记忆"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 同步与生效"]
direction LR
R1N0["把同一状态同步给对耳"]
R1N1["回复当前 App<br/>双 App 场景下更新另一端"]
R1N2["经过防抖后<br/>播放提示音并应用实际模式"]
R1N0 --> R1N1 --> R1N2
end
class R1N0,R1N1,R1N2 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;请求进入提示音模块前会带上“来自 App”的标记。这样提示音结束后只应用实际模式,不会重复修改记忆和上报同一次选择。
按键切换路径
按键路径需要先读取用户在 App 中配置的切换组合,当前代码支持:
- 关闭、降噪和通透循环;
- 关闭和降噪切换;
- 关闭和通透切换;
- 降噪和通透切换。
如果下一步进入降噪,还会读取当前场景保存的降噪等级,选择对应提示音。
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 确定切换目标"]
direction LR
R0N0["识别按键配置和当前运行模式"]
R0N1["计算下一个模式"]
R0N2["选择对应的 ANC 提示音"]
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/>更新单双耳记忆"]
R1N2["应用实际模式<br/>保存并上报 App"]
R1N0 --> R1N1 --> R1N2
end
class R1N0,R1N1,R1N2 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;两条路径的差别主要在提示音回调:App 路径已经更新过状态,回调只应用实际模式;按键路径还要更新记忆、保存并上报 App。从耳发起按键时,提示音请求会交给主耳协调,最终由主耳完成上报。
四、单双耳记忆怎样跟随出入盒和 TWS 变化
当前使用哪份 ANC 记忆,主要看 TWS 是否连接,以及两只耳机是否都在盒外。
| TWS 状态 | 出入盒状态 | 当前处理 |
|---|---|---|
| 未连接 | 当前耳机在盒外 | 使用单耳记忆 |
| 已连接 | 一只在盒内,一只在盒外 | 盒外耳机使用单耳记忆 |
| 已连接 | 两只都在盒外 | 两边使用双耳记忆 |
| 已连接 | 两只都在盒内 | 当前 ANC 关闭,保留两份记忆 |
假设单耳记忆为通透,双耳记忆为降噪,一次快速出盒可能经历下面几个状态:
%%{init: {"flowchart": {"htmlLabels": true, "wrappingWidth": 1000, "rankSpacing": 30}, "themeVariables": {"lineColor": "#71829a"}}}%%
flowchart TB
subgraph R0["① 仍按单耳记忆处理"]
direction LR
R0N0["一只耳机在盒外<br/>另一只在盒内"]
R0N1["使用单耳记忆:通透"]
R0N2["另一只耳机出盒<br/>但 TWS 尚未连接"]
R0N0 --> R0N1 --> R0N2
end
class R0N0,R0N1,R0N2 blue;
style R0 fill:#f1f6fd,stroke:#b8cfe8,color:#23456b
subgraph R1["② 同步后重新判断"]
direction LR
R1N0["暂时仍按单耳记忆处理"]
R1N1["TWS 连接<br/>并同步完对耳状态"]
R1N2["重新判断为双耳场景<br/>恢复双耳记忆:降噪"]
R1N0 --> R1N1 --> R1N2
end
class R1N0,R1N1,R1N2 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 可能还没有回连,如果只判断一次,后面就可能一直停留在单耳模式。因此,本耳出盒、对耳出入盒、TWS 断开和重连时,都要重新判断当前应该使用哪份记忆。尤其是 TWS 完成连接后,需要再补一次 ANC 判断,处理快速出盒时来不及切换的问题。
TWS 连接时还会同步两份 ANC 记忆。主耳把单耳、双耳模式和对应等级发给从耳,从耳更新并保存,保证重新组队后的状态一致。
五、提示音和 App 防抖也是切换链路的一部分
在 App 和按键路径中,提示音结束回调还会把提示音资源转换成 ANC 模式,再调用 ANC 接口。因此,提示音的来源和播放顺序也会影响最终结果。双耳连接时,从耳的提示音请求会交给主耳协调,保证两边同步播放。
App 快速连续切换时,当前代码做了两层处理:
- App 防抖:每条指令仍然更新最新记忆并同步对耳,但中间操作不逐个播放提示音;停止操作 1 秒后,再执行最后一次选择。
- 队列清理:新的 ANC 提示音入队前,先清理队列中等待播放的旧 ANC 提示音。
前者合并连续操作,后者清掉已经进入提示音模块的旧请求,目的都是让最终模式尽快与 App 的最后一次选择保持一致。
六、最后落到硬件、保存和 App 显示
应用层确定目标后,还要转换成 SDK 使用的模式。原厂 SDK 最基本的是关闭、降噪和通透;当前实现增加了中、低和自适应降噪,它们仍属于降噪模式,只是加载的参数不同。
进入 ANC 接口后,先检查功能是否使能、本耳是否在盒内,以及是否需要重复设置。随后根据目标模式停止 ANC,或者加载降噪、通透参数并启动 ANC。盒内耳机会跳过硬件操作,但记忆仍可保留,等待出盒后恢复。
最后,单耳和双耳记忆写入 Flash,App 则根据当前场景显示关闭、单耳记忆或双耳记忆。主动上报由主耳完成,避免左右耳重复通知。
回头看这条链路,我觉得核心就是把六个环节对应起来:触发来源、单双耳场景、目标记忆、提示音、硬件模式和 App 状态。如果模式链路已经正确,实际降噪效果仍有差异,再继续检查声学参数、校准值和算法状态。