通过一个剧院音响师的故事,为你详细解释不同音频内容类型的区别,特别是当设置为CONTENT_TYPE_SPEECH时,其他应用申请AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时的行为差异。
故事背景:多功能剧院
想象你是一家多功能剧院的音响师,负责管理各种演出:
- 🎤 演讲厅:语音内容(
CONTENT_TYPE_SPEECH) - 🎵 音乐厅:音乐演出(
CONTENT_TYPE_MUSIC) - 🎬 电影院:电影放映(
CONTENT_TYPE_MOVIE) - 🔔 提示区:系统通知(
CONTENT_TYPE_SONIFICATION)
今天剧院同时有多个活动:
- 演讲厅正在进行TED演讲(你的应用,设为
CONTENT_TYPE_SPEECH) - 音乐厅有交响乐演出
- 提示区需要播放重要通知
第一章:内容类型的核心区别
java
// 设置不同内容类型的音频属性
AudioAttributes speechAttributes = new AudioAttributesCompat.Builder()
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
.build();
AudioAttributes musicAttributes = new AudioAttributesCompat.Builder()
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC)
.build();
AudioAttributes movieAttributes = new AudioAttributesCompat.Builder()
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE)
.build();
音响师的处理差异
| 内容类型 | 音响系统优化 | 典型场景 | 延迟要求 | 动态范围 |
|---|---|---|---|---|
| 语音 | 增强人声频段(300-3400Hz),降噪 | TED演讲,播客 | <100ms | 窄 |
| 音乐 | 全频段优化,低音增强 | 交响乐,流行音乐 | <500ms | 宽 |
| 电影 | 环绕声优化,动态范围保持 | 电影,游戏 | <200ms | 非常宽 |
| 提示音 | 确保可听性,短促清晰 | 通知,闹钟 | <100ms | 固定 |
第二章:焦点冲突场景 - 重要通知来了
java
// 通知系统申请焦点
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(
null, // 焦点监听器
AudioManager.STREAM_NOTIFICATION,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK // 关键参数
);
焦点申请参数解析:
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:临时焦点,允许其他应用降低音量继续播放
第三章:不同内容类型的响应差异
1. 音乐厅响应(CONTENT_TYPE_MUSIC)
java
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// 降低音量继续播放
exoPlayer.setVolume(0.3f);
Log.d("MusicPlayer", "音乐厅:降低音量继续演奏");
}
}
音响师决策:将交响乐音量降低到30%,继续演奏
2. 电影院响应(CONTENT_TYPE_MOVIE)
java
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// 电影可能降低音量或暂停
if (isDialogueScene()) {
exoPlayer.pause(); // 对白场景暂停
} else {
exoPlayer.setVolume(0.4f); // 动作场景降低音量
}
}
}
音响师决策:根据电影场景决定 - 对白场景暂停,动作场景降低音量
3. 演讲厅响应(CONTENT_TYPE_SPEECH)
java
@Override
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// 对于语音内容,选择暂停而不是降低音量
wasPlaying = exoPlayer.isPlaying();
exoPlayer.pause();
Log.d("SpeechPlayer", "演讲厅:暂停演讲,保证清晰度");
}
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// 焦点恢复时继续播放
if (wasPlaying) {
exoPlayer.play();
}
}
}
音响师决策:完全暂停TED演讲,而不是降低音量
第四章:为什么语音内容需要特殊处理?
技术原因分析
-
清晰度优先:
- 人耳对语音的清晰度要求极高
- 降低音量会导致辅音丢失(如s, t, p等音)
java
// 语音处理优化 if (contentType == CONTENT_TYPE_SPEECH) { enableSpeechEnhancement(true); // 开启语音增强 } -
心理声学效应:
- 背景噪音会显著降低语音理解度
- 研究显示,即使降低50%音量,语音理解度也会下降30%
-
Android官方建议:
"对于语音内容(CONTENT_TYPE_SPEECH),当收到AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK时,应该暂停播放而不是降低音量,因为降低音量会严重影响语音可懂度"
正确实现代码
java
public class SpeechPlayer implements AudioManager.OnAudioFocusChangeListener {
private SimpleExoPlayer exoPlayer;
private boolean wasPlayingBeforeDuck = false;
public void init(Context context) {
// 设置语音内容类型
AudioAttributes attributes = new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SPEECH)
.setUsage(C.USAGE_VOICE_COMMUNICATION)
.build();
exoPlayer = new SimpleExoPlayer.Builder(context)
.setAudioAttributes(attributes, true)
.build();
// 注册音频焦点监听
AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
am.requestAudioFocus(
this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
);
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
handleDuckRequest();
break;
case AudioManager.AUDIOFOCUS_GAIN:
restorePlayback();
break;
}
}
private void handleDuckRequest() {
// 对于语音内容,暂停而不是降低音量
wasPlayingBeforeDuck = exoPlayer.isPlaying();
if (wasPlayingBeforeDuck) {
exoPlayer.pause();
Log.i("SpeechPlayer", "语音内容暂停,保证清晰度");
}
}
private void restorePlayback() {
if (wasPlayingBeforeDuck) {
exoPlayer.play();
wasPlayingBeforeDuck = false;
Log.i("SpeechPlayer", "恢复语音播放");
}
}
// 特殊处理:当需要与其他语音共存时
public void handleSimultaneousSpeech() {
// 使用更高级的焦点请求
int result = audioManager.requestAudioFocus(
this,
AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE // 独占模式
);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 暂停其他语音播放
}
}
}
第五章:高级场景 - 语音优先策略
场景:语音导航与音乐播放冲突
java
public class NavigationManager {
public void playNavigationInstruction(String instruction) {
// 1. 创建语音属性
AudioAttributes navAttributes = new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SPEECH)
.setUsage(C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
.build();
// 2. 请求特殊焦点 - 临时独占
int result = audioManager.requestAudioFocus(
focusListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE // 关键参数
);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 3. 暂停背景音乐
backgroundMusic.pause();
// 4. 播放导航语音
exoPlayer.setAudioAttributes(navAttributes);
exoPlayer.playInstruction(instruction);
// 5. 播放完成后恢复音乐
exoPlayer.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_ENDED) {
backgroundMusic.resume();
audioManager.abandonAudioFocus(focusListener);
}
}
});
}
}
}
焦点请求类型对比
| 请求类型 | 特点 | 适合场景 |
|---|---|---|
| AUDIOFOCUS_GAIN | 长期独占焦点 | 音乐播放器 |
| AUDIOFOCUS_GAIN_TRANSIENT | 短暂暂停他人 | 通知音 |
| AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK | 允许他人降低音量 | 导航提示 |
| AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | 短暂独占焦点 | 语音指令 |
第六章:最佳实践总结
语音内容处理黄金法则
-
设置正确的内容类型:
java
// 必须明确设置为SPEECH .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) -
焦点响应策略:
- 收到
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK时:暂停播放 - 收到
AUDIOFOCUS_LOSS_TRANSIENT时:暂停播放 - 收到
AUDIOFOCUS_LOSS时:停止播放并释放资源
- 收到
-
恢复播放策略:
java
case AudioManager.AUDIOFOCUS_GAIN: if (shouldResumePlayback()) { player.play(); } break; -
特殊场景处理:
- 导航语音:使用
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - 电话接入:完全停止并保存状态
- 同时播放多个语音:实现混音队列
- 导航语音:使用
不同内容类型响应对照表
| 内容类型 | 收到AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时 |
|---|---|
| 语音 | 暂停播放(保证清晰度) |
| 音乐 | 降低音量到20-50% |
| 电影 | 根据场景选择暂停或降低音量 |
| 提示音 | 通常已播放完毕,无需处理 |
| 未知 | 降低音量(保守处理) |
第七章:真实应用示例 - 播客播放器
java
public class PodcastPlayer implements AudioManager.OnAudioFocusChangeListener {
private SimpleExoPlayer player;
private boolean pausedForDuck = false;
public void playPodcast(Uri uri) {
// 设置语音属性
AudioAttributes attributes = new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SPEECH)
.setUsage(C.USAGE_MEDIA)
.build();
player = new SimpleExoPlayer.Builder(context)
.setAudioAttributes(attributes, true)
.build();
// 请求焦点
AudioManager am = (AudioManager) context.getSystemService(AUDIO_SERVICE);
int result = am.requestAudioFocus(
this,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
player.setMediaItem(MediaItem.fromUri(uri));
player.prepare();
player.play();
}
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 语音内容:暂停而不是降低音量
if (player.isPlaying()) {
player.pause();
pausedForDuck = true;
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
player.pause();
break;
case AudioManager.AUDIOFOCUS_GAIN:
if (pausedForDuck || !player.isPlaying()) {
player.play();
pausedForDuck = false;
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
player.stop();
release();
break;
}
}
// 处理导航打断
public void onNavigationStart() {
// 暂停播放但不放弃焦点
player.pause();
}
public void onNavigationEnd() {
// 恢复播放
if (player != null && !player.isPlaying()) {
player.play();
}
}
}
结论:音响师的决策艺术
在Android音频系统中:
-
内容类型决定系统优化方向
- 语音:清晰度优先
- 音乐:音质优先
- 电影:同步和声场优先
-
语音内容的特殊处理
- 当收到
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时,应该暂停播放而非降低音量 - 这是为了保持语音清晰度和可理解性
- Android官方推荐的最佳实践
- 当收到
-
正确实现策略
java
if (contentType == CONTENT_TYPE_SPEECH) { player.pause(); // 语音内容暂停 } else { player.setVolume(0.3f); // 其他内容降低音量 }
记住音响师的黄金法则:
"语音是信息的载体,清晰度胜过连续性。当重要通知来临,暂停演讲是对听众最大的尊重。"
通过正确设置内容类型和实现焦点处理,你的应用将能提供专业级的音频体验!