Usage 和 StreamType 是 Android 音频系统中两代不同的音频分类机制 。简单来说:StreamType 是旧的、简单的分类,音量流类型,Usage 是新的、更精细的分类,音频场景类型。
核心区别总结
| 维度 | StreamType (旧) | AudioAttributes.Usage (新) |
|---|---|---|
| 引入时间 | Android 2.3 及更早 | Android 5.0 (Lollipop) |
| 设计理念 | 基于播放流类型 | 基于音频使用场景 |
| 数量 | 固定的 7-8 种类型 | 灵活扩展,标准 10+ 种 |
| 音量控制 | 1:1 绑定到音量条 | 可灵活映射到音量条 |
| 优先级 | 隐含在类型中 | 显式优先级控制 |
| 扩展性 | 固定,不可扩展 | 可自定义扩展 |
| 推荐使用 | 已废弃,仅兼容旧代码 | Android 5.0+ 推荐 |
详细对比
1. 定义和范围
StreamType(有限、固定)
java
// AudioManager.java
public static final int STREAM_VOICE_CALL = 0; // 通话
public static final int STREAM_SYSTEM = 1; // 系统声音
public static final int STREAM_RING = 2; // 铃声
public static final int STREAM_MUSIC = 3; // 媒体(音乐、视频、游戏)
public static final int STREAM_ALARM = 4; // 闹钟
public static final int STREAM_NOTIFICATION = 5; // 通知
public static final int STREAM_DTMF = 8; // 双音多频
public static final int STREAM_ACCESSIBILITY = 10; // 无障碍
// 总共只有 8 种,且不可扩展
Usage(丰富、灵活)
java
// AudioAttributes.java
public static final int USAGE_UNKNOWN = 0;
public static final int USAGE_MEDIA = 1; // 媒体
public static final int USAGE_VOICE_COMMUNICATION = 2; // 语音通话
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = 3; // 通话信令
public static final int USAGE_ALARM = 4; // 闹钟
public static final int USAGE_NOTIFICATION = 5; // 通知
public static final int USAGE_NOTIFICATION_TELEPHONY_RINGTONE = 6; // 电话铃声
public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = 7; // 通信请求
public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = 8; // 即时消息
public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = 9; // 延迟消息
public static final int USAGE_NOTIFICATION_EVENT = 10; // 事件
public static final int USAGE_ASSISTANCE_ACCESSIBILITY = 11; // 无障碍
public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12; // 导航
public static final int USAGE_ASSISTANCE_SONIFICATION = 13; // UI提示音
public static final int USAGE_GAME = 14; // 游戏
public static final int USAGE_VIRTUAL_SOURCE = 15; // 虚拟声源
public static final int USAGE_ASSISTANT = 16; // 语音助手
// 还可由厂商自定义扩展
2. 设计哲学差异
StreamType:"我在播放什么类型的音频流?"
- 关注音频的技术来源类型
- 简单但粗粒度,难以满足复杂场景
- 一个 StreamType 包含多种语义场景
java
// ❌ StreamType 的问题:过于笼统
// STREAM_MUSIC 包含了:
// 1. 背景音乐
// 2. 游戏音效
// 3. 视频音频
// 4. 播客
// 5. UI 音效
// 系统无法区分这些场景的不同需求
Usage:"这段音频的用途和场景是什么?"
- 关注音频的使用场景和目的
- 细粒度,可精确描述意图
- 系统可做更智能的决策
java
// ✅ Usage 的精确描述
.setUsage(USAGE_GAME) // 游戏音效:可能启用低延迟
.setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) // 导航:可打断音乐
.setUsage(USAGE_NOTIFICATION_EVENT) // 日历提醒:需要确保用户听到
.setUsage(USAGE_ASSISTANT) // 语音助手:需要特殊音频路由
3. 实际使用方式对比
StreamType 方式(旧 API)
java
// MediaPlayer
MediaPlayer player = new MediaPlayer();
player.setAudioStreamType(AudioManager.STREAM_MUSIC); // 设置流类型
player.setDataSource("music.mp3");
player.prepare();
player.start();
// 设置音量(全局影响)
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0);
// SoundPool
SoundPool sp = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(null) // ❌ 老方式,用不了 Usage
.build();
// SoundPool 默认只能用 STREAM_MUSIC
AudioAttributes(新 API,包含 Usage)
java
// 1. 创建 AudioAttributes(包含 usage 和其他属性)
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME) // 使用场景:游戏
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) // 内容类型:音效
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED) // 标志:强制可听
.setHapticChannelsMuted(false) // 触觉通道
.build();
// 2. MediaPlayer(推荐方式)
MediaPlayer player = new MediaPlayer();
player.setAudioAttributes(attributes); // ✅ 设置完整的音频属性
player.setDataSource("game_sound.mp3");
player.prepare();
player.start();
// 3. AudioTrack
AudioTrack track = new AudioTrack.Builder()
.setAudioAttributes(attributes) // ✅
.setAudioFormat(format)
.setBufferSizeInBytes(bufferSize)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
// 4. SoundPool(Android 5.0+)
SoundPool sp = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(attributes) // ✅ 终于可以指定 usage 了!
.build();
// 5. 音量控制(更精确)
// 不再直接控制 STREAM_MUSIC,而是通过 usage
int volume = audioManager.getStreamVolume(
AudioAttributes.usageToStream(AudioAttributes.USAGE_GAME)
);
4. 映射关系
系统内部会将 Usage 映射到 StreamType 以保持兼容性:
java
// frameworks/base/media/java/android/media/AudioAttributes.java
public static int usageToStream(int usage) {
switch (usage) {
case USAGE_UNKNOWN:
return AudioManager.STREAM_MUSIC;
case USAGE_MEDIA:
case USAGE_GAME:
case USAGE_ASSISTANCE_ACCESSIBILITY:
case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: // 导航映射到音乐流
return AudioManager.STREAM_MUSIC;
case USAGE_ASSISTANCE_SONIFICATION:
return AudioManager.STREAM_SYSTEM;
case USAGE_VOICE_COMMUNICATION:
return AudioManager.STREAM_VOICE_CALL;
case USAGE_VOICE_COMMUNICATION_SIGNALLING:
return AudioManager.STREAM_DTMF;
case USAGE_ALARM:
return AudioManager.STREAM_ALARM;
case USAGE_NOTIFICATION:
case USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
case USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
case USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
case USAGE_NOTIFICATION_EVENT:
case USAGE_NOTIFICATION_TELEPHONY_RINGTONE:
return AudioManager.STREAM_NOTIFICATION;
default:
return AudioManager.STREAM_MUSIC;
}
}
注意 :这个映射是有损的,丢失了 Usage 的精细信息。
5. 音频焦点处理差异
StreamType 时代的焦点
java
// 旧方式:基于流类型的简单焦点
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.requestAudioFocus(
focusChangeListener,
AudioManager.STREAM_MUSIC, // 只能指定流类型
AudioManager.AUDIOFOCUS_GAIN
);
// 问题:所有 STREAM_MUSIC 都被同样对待
// 游戏、音乐、视频、导航播报无法区分优先级
AudioAttributes 时代的焦点
java
// 新方式:基于 Usage 的精确焦点
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) // 明确是导航
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setAudioAttributes(attributes) // ✅ 携带详细的音频属性
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChangeListener)
.setWillPauseWhenDucked(false)
.build();
int result = audioManager.requestAudioFocus(focusRequest);
// 系统可以:
// 1. 知道这是导航语音,可适当打断音乐
// 2. 但不是紧急通知,不需要完全暂停
// 3. 可采用闪避(ducking)而非暂停
6. 音频路由策略差异
StreamType 路由
java
// 简单路由:基于设备类型
if (streamType == AudioManager.STREAM_VOICE_CALL) {
routeTo(DEVICE_EARPIECE); // 通话只能路由到听筒
} else if (streamType == AudioManager.STREAM_MUSIC) {
routeTo(DEVICE_SPEAKER); // 音乐只能路由到扬声器
}
// 不智能,无法处理复杂场景
Usage 路由
java
// 智能路由:基于使用场景
switch (usage) {
case USAGE_VOICE_COMMUNICATION:
// 语音通话:优先蓝牙耳机,其次听筒
if (bluetoothAvailable) routeTo(BLUETOOTH_HEADSET);
else routeTo(EARPIECE);
break;
case USAGE_MEDIA:
// 媒体播放:根据上下文选择
if (headphonesConnected) routeTo(HEADPHONES);
else if (carMode) routeTo(CAR_SPEAKERS);
else routeTo(SPEAKER);
break;
case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
// 导航:车载系统特殊处理
if (inCar) routeTo(CAR_FRONT_SPEAKERS);
else routeTo(SPEAKER);
break;
case USAGE_ASSISTANT:
// 语音助手:可能用智能音箱
if (smartSpeakerAvailable) routeTo(SMART_SPEAKER);
else routeTo(DEVICE_DEFAULT);
break;
}
7. 音量控制差异
StreamType 音量控制
java
// 7个独立的音量条
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// 媒体音量(控制音乐、游戏、视频、导航等所有媒体)
am.setStreamVolume(AudioManager.STREAM_MUSIC, mediaVolume, 0);
// 铃声音量
am.setStreamVolume(AudioManager.STREAM_RING, ringVolume, 0);
// 闹钟音量
am.setStreamVolume(AudioManager.STREAM_ALARM, alarmVolume, 0);
// 通话音量
am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, callVolume, 0);
// 问题:导航音量调整会影响音乐音量!
Usage 音量控制
java
// 更细粒度的音量控制(概念上)
// 注意:Android 仍然通过 StreamType 控制音量
// 但通过 usage 到 stream 的映射,可以实现更合理控制
// 系统内部可以根据 usage 做特殊处理
if (usage == USAGE_ASSISTANCE_NAVIGATION_GUIDANCE) {
// 导航语音:可以在媒体音量基础上做偏移
float navigationGain = calculateNavigationGain(mediaVolume);
applyVolume(navigationGain);
} else if (usage == USAGE_GAME) {
// 游戏音效:可单独记忆音量
int gameVolume = getSavedVolume(USAGE_GAME);
am.setStreamVolume(AudioManager.STREAM_MUSIC, gameVolume, 0);
}
迁移指南
从 StreamType 迁移到 Usage
java
// ❌ 旧的 StreamType 方式(已废弃)
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
am.requestAudioFocus(listener, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
// ✅ 新的 AudioAttributes 方式
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA) // 对应 STREAM_MUSIC
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
player.setAudioAttributes(attributes);
AudioFocusRequest request = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attributes)
.setOnAudioFocusChangeListener(listener)
.build();
am.requestAudioFocus(request);
映射参考表
| StreamType | 建议的 Usage | 说明 |
|---|---|---|
STREAM_MUSIC |
USAGE_MEDIA |
普通媒体播放 |
STREAM_MUSIC |
USAGE_GAME |
游戏音效 |
STREAM_MUSIC |
USAGE_ASSISTANCE_NAVIGATION_GUIDANCE |
导航语音 |
STREAM_ALARM |
USAGE_ALARM |
闹钟 |
STREAM_RING |
USAGE_NOTIFICATION_TELEPHONY_RINGTONE |
电话铃声 |
STREAM_NOTIFICATION |
USAGE_NOTIFICATION |
普通通知 |
STREAM_NOTIFICATION |
USAGE_NOTIFICATION_EVENT |
事件提醒 |
STREAM_VOICE_CALL |
USAGE_VOICE_COMMUNICATION |
语音通话 |
STREAM_SYSTEM |
USAGE_ASSISTANCE_SONIFICATION |
系统音效 |
STREAM_ACCESSIBILITY |
USAGE_ASSISTANCE_ACCESSIBILITY |
无障碍 |
实际建议
什么时候用 StreamType?
- ✅ 维护遗留代码(Android 4.x 及更早)
- ✅ 简单的音频播放需求
- ✅ 不需要精细的音频策略控制
什么时候必须用 AudioAttributes.Usage?
- ✅ Android 5.0+ 的新应用
- ✅ 需要细粒度音频控制
- ✅ 需要精确的音频焦点处理
- ✅ 车载、电视、手表等特殊设备
- ✅ 语音助手、导航、游戏等特殊场景
- ✅ 需要控制音频路由(如强制蓝牙输出)
实际代码示例对比
java
// 场景:游戏应用播放音效
// ❌ 旧方式(问题:无法区分游戏和音乐)
soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
// 用户调整媒体音量时,游戏音效和音乐一起被调整
// ✅ 新方式(正确:明确标记为游戏)
AudioAttributes gameAttrs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
soundPool = new SoundPool.Builder()
.setMaxStreams(10)
.setAudioAttributes(gameAttrs) // 明确是游戏
.build();
// 系统可以:
// 1. 启用低延迟模式
// 2. 应用游戏音效优化
// 3. 在音频焦点冲突时合理处理
总结
StreamType 是"流类型"思维,Usage 是"使用场景"思维:
- StreamType 回答:"这是什么类型的音频流?"
- Usage 回答:"这段音频是用来干什么的?"
迁移到 Usage 的优势:
- 更精细的控制:可区分音乐、游戏、导航、通知等
- 更智能的路由:系统可根据场景选择最佳输出设备
- 更合理的焦点:不同场景有不同优先级
- 更好的兼容性:适应车载、电视、穿戴设备等新场景
- 未来扩展性:可自定义新的 usage 类型
对于新应用,强烈建议使用 AudioAttributes.Usage,它提供了更现代、更精确的音频控制方式。