Android音频焦点及混音策略
Posted 村里小码农
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android音频焦点及混音策略相关的知识,希望对你有一定的参考价值。
1.前言
1.1 音频焦点官方解读
两个或两个以上的 android 应用可同时向同一输出流播放音频。系统会将所有音频流混合在一起。虽然这是一项出色的技术,但却会给用户带来很大的困扰。为了避免所有音乐应用同时播放,Android 引入了“音频焦点”的概念。 一次只能有一个应用获得音频焦点。
当您的应用需要输出音频时,它需要请求获得音频焦点,获得焦点后,就可以播放声音了。不过,在您获得音频焦点后,您可能无法将其一直持有到播放完成。其他应用可以请求焦点,从而占有您持有的音频焦点。如果发生这种情况,您的应用应暂停播放或降低音量,以便于用户听到新的音频源。
1.2 案例说明解读
1).如果手机上安装了两个音频播放器,当一个正在播放的时候,打开第二个播放歌曲,有没有发现第一个自动暂停了。
2).如果你在听音频的同时,又去打开了其它视频APP,你会发现音频APP暂停播放了。
3).如果你正在听音频或者看视频时,来电话了,那么音视频便会暂停。挂了电话后音乐又继续播放,视频则需要点击按钮播放,是不是很奇怪
4).当你收到消息,比如微信消息,并且有消息声音的时候,那么听音频的那一瞬间,音频的声音会变小了,然后过会儿又恢复了。是不是很有意思。
别蒙圈,这个就叫做音频捕获和丢弃焦点。
1.3为什么要处理音频焦点问题
如果不处理捕获与丢弃音频焦点的话,那么同时开几个音视频播放器,就会出现多个声音;
这是一种不好的体验。如下图:
图1
图2
1.4 Vehicle为什么要混音呢?
Vehicle混音场景使用较频繁,常用的场景如下:
- 媒体播放(音乐 + 收音 + 视频) + 导航场景
- 媒体播放(音乐 + 收音 + 视频) + 语音提示
-
2. APP规范化
2.1 遵守音频焦点准则
行为恰当的音频应用应根据以下一般准则来管理音频焦点:
- 在即将开始播放之前调用 requestAudioFocus(),并验证调用是否返回 AUDIOFOCUS_REQUEST_GRANTED。如果按照本指南中的说明设计应用,则应在媒体会话的 onPlay() 回调中调用 requestAudioFocus()。
- 在其他应用获得音频焦点时,停止或暂停播放,或降低音量。
- 播放停止后,放弃音频焦点。
运行的 Android 版本不同,音频焦点的处理方式也会不同:
- 从Android 2.2(API 级别 8)开始,应用通过调用 requestAudioFocus() 和 abandonAudioFocus() 来管理音频焦点。应用还必须为这两个调用注册 AudioManager.OnAudioFocusChangeListener,以便接收回调并管理自己的音量。
- 对于以 Android 5.0(API 级别 21)及更高版本为目标平台的应用,音频应用应使用 AudioAttributes 来描述应用正在播放的音频类型。例如,播放语音的应用应指定 CONTENT_TYPE_SPEECH。
- 面向 Android 8.0(API 级别 26)或更高版本的应用应使用 requestAudioFocus() 方法,该方法会接受 AudioFocusRequest 参数。AudioFocusRequest 包含有关应用的音频上下文和功能的信息。系统使用这些信息来自动管理音频焦点的得到和失去。
2.2 请求音频焦点
获取音频焦点的第一个步骤是先向系统发出申请焦点的消息。注意这只是发出请求,并非直接获取。为了申请到音频聚焦,您必须向系统描述好您的意图。介绍四个常见音频焦点类型:
- AUDIOFOCUS_GAIN的使用场景:应用需要聚焦音频的时长会根据用户的使用时长改变,属于不确定期限。例如:多媒体播放或者播客等应用。
- AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK的使用场景:应用只需短暂的音频聚焦,来播放一些提示类语音消息,或录制一段语音。例如:闹铃,导航等应用。
- AUDIOFOCUS_GAIN_TRANSIENT的使用场景:应用只需短暂的音频聚焦,但包含了不同响应情况,例如:电话、QQ、微信等通话应用。
- AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE 的使用场景:同样您的应用只是需要短暂的音频聚焦。未知时长,但不允许被其它应用截取音频焦点。例如:录音软件。
在音频焦点成功获取后,该方法会返回AUDIOFOCUS_REQUEST_GRANTED常量,否则,会返回AUDIOFOCUS_REQUEST_FAILED常量。
音频焦点处理逻辑:
- 在service的oncreate方法中调用初始化方法
- 在播放音频的时候开始请求捕获音频焦点
- 在音频销毁的时候开始丢弃音频焦点
1). Android O之前版本,需要用到 AudioFocusRequest,只需实现 AudioManager.OnAudioFocusChangeListener 接口。代码如下:
AudioManager mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
int focusRequest = mAudioManager.requestAudioFocus(
..., // Need to implement listener
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
switch (focusRequest)
case AudioManager.AUDIOFOCUS_REQUEST_FAILED:
//请求焦点失败-->不允许播放
case AudioManager.AUDIOFOCUS_REQUEST_GRANTED:
//请求焦点成功->开始播放
2). Android O以及更新的版本上 必须使用 builder 来实例化一个 AudioFocusRequest 类。(在 builder 中必须指明请求的音频焦点类型),接口代码如下:
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(afChangeListener, handler)
.build();
mediaPlayer = new MediaPlayer();
final Object focusLock = new Object();
boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;
// ...
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock)
if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED)
//请求焦点失败-->不允许播放
else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
//请求焦点成功->开始播放
else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED)
//延时焦点
// ...
延时焦点-场景案例:
假如当用户在通话中打开游戏,他们想玩游戏,不想听到游戏声音。但是当他们通话结束的时候他们想听到游戏声音(通话应用暂时持有音频焦点)。如果您的应用支持延迟音频聚焦,会发生如下情况:
- 当您的应用申请音频焦点的时候,会被拒绝并锁住,通话应用继续持有音频焦点,您的应用因此不播放音频。因为您的应用是游戏,可以正常继续操作,只是没有声音。
- 当通话结束,您的应用会被授权延迟音频聚焦。这个授权是来自刚才申请音频聚焦被拒绝后锁住的那个请求,它只是被延迟一段时间后再授权给您。您可以像上文建议应对音频焦点得失的处理方式那样处理,在本例中,此时便可以开始恢复播放。 目前低于 Android O 的版本是不支持延迟音频聚焦这个功能的,所以本用例在其它版本下,应用并不会延迟获得音频焦点。
2.3 响应音频焦点的状态改变
一旦获得音频聚焦,您的应用要马上做出响应,因为它的状态可能在任何时间发生改变(丢失或重新获取),您可以实现 OnAudioFocusChangeListener 的来响应状态改变。
以下代码展示了 OnAudioFocusChangeListener 接口的实现,它处理了与 Google Assistant 应用协同工作的时候,音频焦点的各种状态的变化。
状态说明:
- AUDIOFOCUS_GAIN:重新获取音频焦点。
- AUDIOFOCUS_LOSS:永久性失去音频焦点,则其他应用会播放音频。您的应用应立即暂停播放,清理资源;因为它不会收到 AUDIOFOCUS_GAIN 回调。
- AUDIOFOCUS_ LOSS_TRANSIENT:暂时失去音频焦点,但是很快就会重新获得,在此状态应该暂停所有音频播放,但是不能清除资源,
- AUDIOFOCUS_ LOSS_TRANSIENT _CAN_DUCK:暂时失去音频焦点,应用应该降低音量(如果您不依赖于自动降低音量),允许持续播放音频(以很小的声音),不需要完全停止播放。
//焦点状态改变
@Override
public void onAudioFocusChange(int focusChange)
int volume;
switch (focusChange)
// 重新获得焦点
//如果通话结束,恢复播放;获取音量并且恢复音量。这个情景应该经常遇到
case AudioManager.AUDIOFOCUS_GAIN:
if (!willPlay() && isPausedByFocusLossTransient)
// 通话结束,恢复播放
mPlayService.playPause();
//获取音量
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume ==
mVolumeWhenFocusLossTransientCanDuck / 2)
// 恢复音量
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
mVolumeWhenFocusLossTransientCanDuck, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
isPausedByFocusLossTransient = false;
mVolumeWhenFocusLossTransientCanDuck = 0;
break;
// 永久丢失焦点,如被其他播放器抢占,释放焦点
// 必须停止所有的audio播放,清理资源
case AudioManager.AUDIOFOCUS_LOSS:
if (willPlay())
forceStop();
break;
// 短暂丢失焦点,比如来了电话或者微信视频音频聊天等等
// 但是很快就会重新获得,在此状态应该暂停所有音频播放,但是不能清除资源
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if (willPlay())
forceStop();
isPausedByFocusLossTransient = true;
break;
// 瞬间丢失焦点,如通知,导航
// 暂时失去 audio focus,但是允许持续播放音频(以很小的声音),不需要完全停止播放。
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 音量减小为一半
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
if (willPlay() && volume > 0)
mVolumeWhenFocusLossTransientCanDuck = volume;
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
mVolumeWhenFocusLossTransientCanDuck / 2,
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
break;
default:
break;
失去焦点的三种类型:
- 失去短暂焦点 (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT)
- 失去永久焦点 (AudioManager.AUDIOFOCUS_LOSS)
- Ducking (AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK)
场景:
- 当永久丢失焦点,比如同时打开播放器,则停止或者暂停播放,否则出现两个声音
- 当短暂丢失焦点,比如比如来了电话或者微信视频音频聊天等等,则暂停或者停止播放
- 当瞬间丢失焦点,比如手机来了通知/导航。前提是你的通知是震动或者声音时,会短暂地将媒体音量减小一半;当然你也可以减小三分之一。
重新获得焦点场景:
- 当重新获得焦点的时候,如果通话结束,恢复播放;获取音量并且恢复音量。这个情景应该经常遇到。
2.4 释放音频焦点
播放完音频,记得使用 AudioManager.abandonAudioFocus(...) 来释放掉音频焦点。在前面的步骤中,我们遇到了一个应用暂停播放应该释放音频焦点的情况,但是这个应用依旧保留了音频焦点。
// Abandon audio focus when playback complete
audioManager.abandonAudioFocus(afChangeListener);
- 如果要释放的client是在栈顶,则释放之后,让下一个栈顶的client获得了音频焦点。
- 如果要释放的client不是在栈顶,则只是移除这个记录,不需要更改当前音频焦点的占有情况。
2.5 APP音频焦点规范化后的效果:如下图
3. 混音策略
3.1 系统软件混音
通过上述音频焦点AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK模式实现混音操作,或者在修改系统框架层,处理导航输出,压低媒体声音实现混音效果。
3.2 硬件混音
参考框架图:
如上硬件框架,软件设计如下:
- NAV + Media混音时候,FM声道Mute,同时压低Media音量。
- NAV + FM混音时候,Media声道Mute,同时压低FM音量。
4. 异常情况处理
4.1 导航APP未按音频焦点规范,系统如何处理?//主要涉及系统软件混音
处理方案如下:
- 软件混音:系统Framework层将第三方导航的声音绑定到STREAM_ALARM通道, 系统媒体声音绑定到STREAM_MUSIC通道,通过设置不同通道的音量来实现混音效果。
- 如何获取当前使用的APP包名?
方法:ActivityThread.currentPackageName();//获取该Class在哪个package调用
//AudioAttributes 参考修改
Public AudioAttributes build()
...
String mCurName = ActivityThread.currentPackageName();
if(mCurName != null && mCurName .equals(“com.android.nav”))
//重置媒体ContentType
mContentType = AudioManager.STREAM_ALARM;
//重置mUsage 类型
mUsage = USAGE_ALARM;
...
- 检测到是第三方导航请求音频焦点,那么通知媒体减低音量
4.2 第三方媒体播放器未按音频焦点规范,系统如何处理?
- 系统层强制切换焦点,将第三方APP焦点移除堆栈(或者清空堆栈),将新请求的焦 点移到栈顶,重新请求焦点。
4.3 未经过SOC处理的收音机FM/AM音频焦点及混音如何处理?
- 通过Native控制FM/AM的源切换
- 通过硬件混音实现
- 软件混音,通过Native控制driver压低FM/AM音量
音频焦点涉及相关代码路径:
frameworks/base/media/java/android/media/AudioFocusRequest.java
frameworks/base/media/java/android/media/AudioAttributes.java
frameworks/base/media/java/android/media/AudioManager.java
frameworks/base/media/java/android/media/Audiosystem.java
frameworks/base/media/java/android/media/AudioFocusInfo.java
frameworks/base/services/core/java/com/android/server/audio/AudioService.java
frameworks/base/services/core/java/com/android/server/audio/FocusRequest.java
frameworks/base/services/core/java/com/android/server/audio/MediaFocusControl.java
Android上一种效果奇好的混音方法介绍
本文将对几种音频混音的方法进行详细的介绍和比较,读完之后你应该可以对混音有个基本的认识,针对不同情形知道应该采用哪种具体的处理方法了。
如果对音频的一些基础知识还不是很了解的建议先去阅读一下上一篇文章:写给小白的音频认识基础 。
混音的原理
音频混音的原理: 空气中声波的叠加等价于量化的语音信号的叠加。
这句话可能有点拗口,我们从程序员的角度去观察就不难理解了。下图是两条音轨的数据,将每个通道的值做线性叠加后的值就是混音的结果了。比如音轨A和音轨B的叠加,A.1
表示 A 音轨的 1 通道的值 AB03
, B.1
表示 B 音轨的 1 通道的值 1122
, 结果是 bc25
,然后按照低位在前的方式排列,在合成音轨中就是 25bc
,这里的表示都是 16 进制的。
直接加起来就可以了?事情如果这么简单就好了。音频设备支持的采样精度肯定都是有限的,一般为 8 位或者 16 位,大一些的为 32 位。在音轨数据叠加的过程中,肯定会导致溢出的问题。为了解决这个问题,人们找了不少的办法。这里我主要介绍几种我用过的,并给出相关代码实现和最终的混音效果对比结果。
线性叠加平均
这种办法的原理非常简单粗暴,也不会引入噪音。原理就是把不同音轨的通道值叠加之后取平均值,这样就不会有溢出的问题了。但是会带来的后果就是某一路或几路音量特别小那么整个混音结果的音量会被拉低。
以下的的单路音轨的音频参数我们假定为采样频率一致,通道数一致,通道采样精度统一为 16 位。
其中参数 bMulRoadAudios
的一维表示的是音轨数,二维表示该音轨的音频数据。
Java 代码实现:
1
|
|
自适应混音
参与混音的多路音频信号自身的特点,以它们自身的比例作为权重,从而决定它们在合成后的输出中所占的比重。具体的原理可以参考这篇论文:快速实时自适应混音方案研究。这种方法对于音轨路数比较多的情况应该会比上面的平均法要好,但是可能会引入噪音。
Java 代码实现:
1
|
|
多通道混音
在实际开发中,我发现上面的两种方法都不能达到满意的效果。一方面是和音乐相关,对音频质量要求比较高;另外一方面是通过手机录音,效果肯定不会太好。不知道从哪里冒出来的灵感,为什么不试着把不同的音轨数据塞到不同的通道上,让声音从不同的喇叭上同时发出,这样也可以达到混音的效果啊!而且不会有音频数据损失的问题,能很完美地呈现原来的声音。
于是我开始查了一下 Android 对多通道的支持情况,对应代码可以在android.media.AudioFormat
中查看,结果如下:
1
|
public static final int CHANNEL_OUT_FRONT_LEFT = 0x4;
|
一共支持 10 个通道,对于我的情况来说是完全够用了。我们的耳机一般只有左右声道,那些更多通道的支持是 Android 系统内部通过软件算法模拟实现的,至于具体如何实现的,我也没有深入了解,在这里我们知道这回事就行了。我们平时所熟知的立体声,5.1 环绕等就是上面那些通道的组合。
1
|
int CHANNEL_OUT_MONO = CHANNEL_OUT_FRONT_LEFT;
|
知道原理之后,实现起来非常简单,下面是具体的代码:
1
|
|
结果比较
线性叠加平均法虽然看起来很简单,但是在音轨数量比较少的时候取得的效果可能会比复杂的自适应混音法要出色。
自适应混音法比较合适音轨数量比较多的情况,但是可能会引入一些噪音。
多通道混音虽然看起来很完美,但是产生的文件大小是数倍于其他的处理方法。
没有银弹,还是要根据自己的应用场景来选择,多试一下。
下面是我录的两路音轨:
-
音轨一:
-
音轨二:
-
线性叠加平均法:
-
自适应混音法:
-
多通道混音:
采样频率、采样精度和通道数不同的情况如何处理?
不同采样频率需要算法进行重新采样处理,让所有音轨在同一采样率下进行混音,这个比较复杂,等有机会再写篇文章介绍。
采样精度不同比较好处理,向上取精度较高的作为基准即可,高位补0;如果是需要取向下精度作为基准的,那么就要把最大通道值和基准最大值取个倍数,把数值都降到最大基准数以下,然后把低位移除。
通道数不同的情况也和精度不同的情况相似处理。
参考资料
来源:https://yedaxia.me/Android-A-Good-MixAudioMethod/
以上是关于Android音频焦点及混音策略的主要内容,如果未能解决你的问题,请参考以下文章