7、音频流类型:流类型定义(STREAM_MUSIC等)、流类型与策略映射、流类型音量管理

各位好,欢迎来到第七章。今天我们来聊聊 Android 音频系统里一个最基础、但也最容易让人混淆的概念——音频流类型

说白了,音频流类型就是给声音贴个标签。你手机里放音乐、来电话、闹钟响、导航播报,这些声音虽然都从扬声器出来,但系统得知道它们谁是谁,才能决定谁该响、谁该停、音量该多大。这就是流类型存在的意义。

我个人习惯把流类型理解成「声音的身份证」。每个音频流在出生时就被打上了类型标签,后续所有的路由策略、音量管理、焦点控制,全都依赖这张身份证来决策。

7.1 流类型定义

Android 在 AudioManager 中定义了一组常量,从 STREAM_VOICE_CALLSTREAM_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,结果音量死活调不对,其实就是流类型选错了。

小提示:Android 10 之后新增了 STREAM_ACCESSIBILITY,专门给 TalkBack 这类无障碍服务用。它的音量是独立管理的,和媒体、通话音量都不挂钩。如果你在做无障碍相关功能,记得用这个流类型。

7.2 流类型与策略映射

流类型定义好了,接下来系统得知道:这个流应该走哪个设备?音量怎么调?能不能和其他流混音?这些决策逻辑,就是音频策略

Android 的音频策略核心在 AudioPolicyManager 里。它维护了一张映射表,把流类型映射到具体的策略规则。我画了一张图,帮你理清这个映射关系:

流类型 → 策略映射关系 音频流类型 STREAM_VOICE_CALL STREAM_SYSTEM STREAM_RING STREAM_MUSIC STREAM_ALARM STREAM_NOTIFICATION STREAM_DTMF STREAM_ACCESSIBILITY 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_MUSICSTREAM_DTMF 在某些设备上共享同一个音量滑块。这取决于 OEM 在 audio_policy_volumes.xml 中的配置。我在项目中遇到过用户反馈:按音量键调节媒体音量,结果拨号键盘音也跟着变了。其实就是因为这两个流被分到了同一个音量组。

避坑指南:如果你在开发 App 时自定义了音频播放,不要随意使用 STREAM_MUSIC 以外的流类型。我曾经见过一个 App 把背景音乐设成了 STREAM_ALARM,结果用户把媒体音量调到最低,闹钟音量却还是震耳欲聋——因为用户根本不知道闹钟音量是独立调节的。用户体验极差。

最后提一下音量持久化。每个流类型的音量索引会被保存到 Settings.System 数据库中。下次开机时,AudioService 会从数据库恢复音量值。但要注意:不同用户(多用户模式)的音量设置是隔离的。如果你在调试时发现音量「莫名其妙」变了,先检查一下是不是切换了用户。

好了,这一章的内容就到这里。流类型是音频系统的基石,理解了它,后面的路由策略、焦点管理、设备切换才能串起来。下一章我们会深入音频焦点——为什么两个 App 同时播放时,一个会被暂停?我们到时候细聊。


公众号:蓝海资料掘金营,微信deep3321