7、音频流类型:流类型定义(STREAM_MUSIC等)、流类型与策略映射、流类型音量管理
各位好,欢迎来到第七章。今天我们来聊聊 Android 音频系统里一个最基础、但也最容易让人混淆的概念——音频流类型。
说白了,音频流类型就是给声音贴个标签。你手机里放音乐、来电话、闹钟响、导航播报,这些声音虽然都从扬声器出来,但系统得知道它们谁是谁,才能决定谁该响、谁该停、音量该多大。这就是流类型存在的意义。
我个人习惯把流类型理解成「声音的身份证」。每个音频流在出生时就被打上了类型标签,后续所有的路由策略、音量管理、焦点控制,全都依赖这张身份证来决策。
7.1 流类型定义
Android 在 AudioManager 中定义了一组常量,从 STREAM_VOICE_CALL 到 STREAM_ACCESSIBILITY,一共十几个。我挑几个最常用的说说:
| 流类型 | 常量值 | 典型用途 |
|---|---|---|
| STREAM_VOICE_CALL | 0 | 通话音频 |
| STREAM_SYSTEM | 1 | 系统提示音(按键音、锁屏声) |
| STREAM_RING | 2 | 来电铃声 |
| STREAM_MUSIC | 3 | 媒体播放、游戏音效 |
| STREAM_ALARM | 4 | 闹钟 |
| STREAM_NOTIFICATION | 5 | 通知提示音 |
| STREAM_DTMF | 8 | 拨号键盘音 |
| STREAM_ACCESSIBILITY | 10 | 无障碍辅助功能音频 |
嗯,这里要注意:STREAM_MUSIC 虽然名字叫「音乐」,但它实际上承载了所有媒体类音频——视频、游戏、播客、导航语音(如果 App 选择用媒体流播放的话)。我在项目中遇到过不少开发者以为导航语音应该走 STREAM_VOICE_CALL,结果音量死活调不对,其实就是流类型选错了。
STREAM_ACCESSIBILITY,专门给 TalkBack 这类无障碍服务用。它的音量是独立管理的,和媒体、通话音量都不挂钩。如果你在做无障碍相关功能,记得用这个流类型。
7.2 流类型与策略映射
流类型定义好了,接下来系统得知道:这个流应该走哪个设备?音量怎么调?能不能和其他流混音?这些决策逻辑,就是音频策略。
Android 的音频策略核心在 AudioPolicyManager 里。它维护了一张映射表,把流类型映射到具体的策略规则。我画了一张图,帮你理清这个映射关系:
你想想看,为什么插上耳机后,媒体声音自动转到耳机,但铃声还是从扬声器出来?这就是策略映射在起作用。系统根据流类型查表,发现 STREAM_MUSIC 的策略是「优先走有线/蓝牙设备」,而 STREAM_RING 的策略是「始终走扬声器(除非用户手动切换)」。这些规则定义在 audio_policy_configuration.xml 里。
我曾经调试过一个 Bug:某款手机插上蓝牙耳机后,微信语音通话的声音还是从听筒出来。查了半天,发现是 App 把语音通话的流类型设成了 STREAM_VOICE_CALL,而蓝牙耳机的策略配置里,STREAM_VOICE_CALL 的默认路由是「听筒+蓝牙并行」。嗯,改一下策略配置就解决了。
7.3 流类型音量管理
每个流类型都有自己独立的音量曲线。Android 用 AudioService 里的 VolumeStreamState 来管理每个流的音量。核心逻辑在 AudioService.setStreamVolume() 里。
我们来看一段关键代码(简化版):
// frameworks/base/services/core/java/com/android/server/audio/AudioService.java
public void setStreamVolume(int streamType, int index, int flags, String callingPackage) {
// 1. 检查流类型是否合法
if (!isValidStreamType(streamType)) {
return;
}
// 2. 获取该流类型的音量状态
VolumeStreamState streamState = mStreamStates[streamType];
// 3. 检查音量索引是否在有效范围内
int maxIndex = streamState.getMaxIndex();
if (index < 0) index = 0;
if (index > maxIndex) index = maxIndex;
// 4. 应用音量(内部会触发音量曲线转换)
streamState.setIndex(index, callingPackage);
// 5. 通知 AudioFlinger 更新硬件音量
sendVolumeUpdate(streamType, index, flags);
}
这里有个细节:音量索引和实际增益之间不是线性关系。Android 内部用了一个对数曲线,把 0-100 的索引映射到实际的 dB 值。为什么?因为人耳对声音的感知是对数的。你从 0 调到 10,感觉变化很大;但从 90 调到 100,感觉变化很小。对数曲线就是为了补偿这种感知特性。
STREAM_ALARM 的曲线可能更陡峭——闹钟音量从 0 到 10 的增益变化比媒体流更大,确保闹钟在低音量下也有足够的响度。
另外,Android 还支持音量分组。比如 STREAM_MUSIC 和 STREAM_DTMF 在某些设备上共享同一个音量滑块。这取决于 OEM 在 audio_policy_volumes.xml 中的配置。我在项目中遇到过用户反馈:按音量键调节媒体音量,结果拨号键盘音也跟着变了。其实就是因为这两个流被分到了同一个音量组。
STREAM_MUSIC 以外的流类型。我曾经见过一个 App 把背景音乐设成了 STREAM_ALARM,结果用户把媒体音量调到最低,闹钟音量却还是震耳欲聋——因为用户根本不知道闹钟音量是独立调节的。用户体验极差。
最后提一下音量持久化。每个流类型的音量索引会被保存到 Settings.System 数据库中。下次开机时,AudioService 会从数据库恢复音量值。但要注意:不同用户(多用户模式)的音量设置是隔离的。如果你在调试时发现音量「莫名其妙」变了,先检查一下是不是切换了用户。
好了,这一章的内容就到这里。流类型是音频系统的基石,理解了它,后面的路由策略、焦点管理、设备切换才能串起来。下一章我们会深入音频焦点——为什么两个 App 同时播放时,一个会被暂停?我们到时候细聊。